dsh-connect-trae 3.6.1 → 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.
@@ -344,9 +351,10 @@ var TraeCatalog = class {
344
351
  *
345
352
  * The mismatch is reachable in BOTH directions, because DSH itself spans both
346
353
  * generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
347
- * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`), while this plugin
348
- * accepts the whole range (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer,
349
- * the other one is wrong:
354
+ * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`, and 0.2.1-rc.1 —
355
+ * the current desktop host — declares **`^1.1.0`**, which also stopped reading
356
+ * the top-level fields entirely), while this plugin accepts the whole range
357
+ * (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer, the other one is wrong:
350
358
  *
351
359
  * | host | resolved api | outcome |
352
360
  * |---|---|---|
@@ -498,24 +506,61 @@ function readManifest(directory) {
498
506
  /** Provider route this bundle owns for the domestic (CN) gateway. */
499
507
  const TRAE_PROVIDER = "trae";
500
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
+ /**
501
522
  * Provider route this bundle owns for the international (trae.ai) gateway.
502
523
  * Named `-global` to match the plugin family's convention (`workbuddy-global`);
503
524
  * the internal region bucket stays `ai`, which is the gateway/protocol name.
504
525
  */
505
526
  const TRAE_AI_PROVIDER = "trae-global";
506
- /** 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. */
507
533
  const TRAE_PROVIDERS = {
508
534
  cn: TRAE_PROVIDER,
509
535
  ai: TRAE_AI_PROVIDER
510
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
+ }
511
546
  /** Human-readable provider names, shown in the DSH model picker. */
512
547
  const TRAE_PROVIDER_DISPLAY_NAMES = {
513
548
  cn: "Trae",
514
549
  ai: "Trae Global"
515
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
+ }
516
561
  /** Region a provider route id belongs to. */
517
562
  function regionOfTraeProvider(provider) {
518
- 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;
519
564
  }
520
565
  const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
521
566
  const INERT_AUTH = {
@@ -548,10 +593,10 @@ const REQUEST_IMAGE_BUDGETS = {
548
593
  * failing the entire provider route (`INVALID_MODEL_CONTEXT`).
549
594
  */
550
595
  const FALLBACK_CONTEXT_WINDOW = 2e5;
551
- function toPiModel(info, baseUrl, providerId) {
596
+ function toPiModel(info, baseUrl, providerId, withRate = true) {
552
597
  return {
553
598
  id: info.id,
554
- name: traeModelDisplayName(info),
599
+ name: traeModelDisplayName(info, withRate),
555
600
  api: "openai-completions",
556
601
  provider: providerId,
557
602
  baseUrl,
@@ -689,13 +734,17 @@ function adaptLegacyPiAiContext(context) {
689
734
  * transcript a MODERN (0.86+) api consumes — the reverse mismatch, and just as
690
735
  * silent.
691
736
  *
692
- * WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1`)
737
+ * WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1`,
738
+ * while the 0.2.1-rc.1 desktop host pins `^1.1.0` and still emits the legacy
739
+ * envelope from its own `piContext()`)
693
740
  * while this plugin may resolve a newer copy than its host. A LEGACY host never
694
741
  * normalizes, so it hands the api the legacy shape; a modern api reads the
695
742
  * prompt from a leading system message and the tools from its `toolsAdded`, and
696
743
  * never looks at the top-level fields — so both are dropped without a sound.
697
744
  * Verified against the published tarballs: the request that reaches the wire
698
- * carries no system turn and `tools: []`.
745
+ * carries no system turn and `tools: []`. This is exactly the shape that
746
+ * `dsh-llm-pi-ai@0.2.1-rc.1` + `pi-ai@1.1.0` would hit unaided, which is why
747
+ * the wire-level matrix in `tests/pi-ai-context.spec.ts` pins both directions.
699
748
  *
700
749
  * Identity for anything already in transcript shape, and for a context with
701
750
  * neither a prompt nor tools (pi-ai's own `createInitialSystemMessage` returns
@@ -730,20 +779,47 @@ function adaptModernPiAiContext(context) {
730
779
  * no context and are passed through untouched, and each adapter function is an
731
780
  * identity unless the context is actually the other generation's shape. So a
732
781
  * healthy install does one `Array.some` per request.
782
+ *
783
+ * THE CAST IS THE CONTRACT (pi-ai 1.1.0). From 1.1.0 `ProviderStreams` takes
784
+ * `TranscriptContext`, a BRANDED type whose `unique symbol` member only
785
+ * `normalizeContext()` can produce — so a `Context` is no longer assignable and
786
+ * `tsc` reports `TS2345` at both call sites below. The brand cannot be acquired
787
+ * honestly here, for two reasons that are both deliberate:
788
+ *
789
+ * 1. This wrapper sits BELOW the normalizer. `Models.stream()`/`streamSimple()`
790
+ * call `normalizeContext()` and hand a real transcript to the provider;
791
+ * `createProvider()` passes it straight through to `api.stream`. So on the
792
+ * modern path `adapt()` is an identity and the value already IS a branded
793
+ * transcript at runtime — the type just cannot see it across that boundary.
794
+ * 2. The legacy path REQUIRES an unbranded shape. pi-ai ≤0.85 has no
795
+ * `normalizeContext` and no transcript type at all, so its api expects
796
+ * exactly the `{ systemPrompt, tools, messages }` object `adapt()` returns.
797
+ *
798
+ * So the value handed to `api` is "a context of the generation that api
799
+ * speaks", which no single edition's declared type names. Casting at this one
800
+ * boundary is what makes both generations expressible; deriving the type from
801
+ * the resolved copy instead would make the wrapper uncompilable against the
802
+ * other generation, which is the whole point of having it.
733
803
  */
734
804
  function withContextAdaptation(api, generation) {
735
805
  const adapt = generation === "modern" ? adaptModernPiAiContext : adaptLegacyPiAiContext;
806
+ const forApi = (context) => adapt(context);
736
807
  return {
737
808
  ...api,
738
- stream: (model, context, options) => api.stream(model, adapt(context), options),
739
- streamSimple: (model, context, options) => api.streamSimple(model, adapt(context), options)
809
+ stream: (model, context, options) => api.stream(model, forApi(context), options),
810
+ streamSimple: (model, context, options) => api.streamSimple(model, forApi(context), options)
740
811
  };
741
812
  }
742
813
  function createTraeAdapter(options) {
743
814
  const providerId = options.provider ?? "trae";
744
815
  const providerName = options.displayName ?? "Trae";
745
816
  const generation = options.piAiGeneration ?? piAiRuntimeInfo().generation;
746
- 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));
747
823
  const provider = {
748
824
  ...createProvider({
749
825
  id: providerId,
@@ -779,7 +855,7 @@ function createTraeAdapter(options) {
779
855
  adapter: new PiAiAdapter({
780
856
  profiles: () => profiles,
781
857
  auth: INERT_AUTH,
782
- resolveApiKey: async () => options.shim.token(),
858
+ resolveApiKey: async () => options.secret ?? options.shim.token(),
783
859
  ...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
784
860
  }),
785
861
  invalidate() {
@@ -788,7 +864,113 @@ function createTraeAdapter(options) {
788
864
  };
789
865
  }
790
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
791
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
+ */
792
974
  /** Plugin-owned usage endpoint consumed by its browser half. */
793
975
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
794
976
  /** Plugin-owned live model refresh endpoint. */
@@ -855,8 +1037,8 @@ const TRAE_REGIONS = ["cn", "ai"];
855
1037
  * Address one region's status route. The two regions are separate provider
856
1038
  * stacks; every card request carries the region whose tab the user is on.
857
1039
  */
858
- function withTraeRegion(path, region) {
859
- return `${path}?${TRAE_REGION_PARAM}=${region}`;
1040
+ function withTraeRegion(path, panel) {
1041
+ return `${path}?${TRAE_REGION_PARAM}=${panel}`;
860
1042
  }
861
1043
  /**
862
1044
  * Read the region parameter off a status-route URL. Absent means the domestic
@@ -867,7 +1049,7 @@ function regionOfTraeStatusUrl(url) {
867
1049
  const at = url.indexOf("?");
868
1050
  const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
869
1051
  if (value === null || value === "") return "cn";
870
- return TRAE_REGIONS.includes(value) ? value : void 0;
1052
+ return TRAE_PANELS.includes(value) ? value : void 0;
871
1053
  }
872
1054
  /**
873
1055
  * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
@@ -944,11 +1126,11 @@ function followAppLoginEnabled(configured) {
944
1126
  * region's picks. Tolerates any stored shape (absent, non-object) by starting
945
1127
  * from an empty document.
946
1128
  */
947
- function nextRegionSlots(regions, region, slot) {
1129
+ function nextRegionSlots(regions, panel, slot) {
948
1130
  const unwrapped = unwrapVolatileDeep(regions);
949
1131
  return {
950
1132
  ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
951
- [region]: slot
1133
+ [panel]: slot
952
1134
  };
953
1135
  }
954
1136
  /**
@@ -973,8 +1155,8 @@ function regionsMapOf(value) {
973
1155
  return record;
974
1156
  }
975
1157
  /** One region's stored slot as a plain object; any other shape reads as empty. */
976
- function regionSlotOf(value, region) {
977
- const slot = unwrapVolatile(regionsMapOf(value)[region]);
1158
+ function regionSlotOf(value, panel) {
1159
+ const slot = unwrapVolatile(regionsMapOf(value)[panel]);
978
1160
  return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
979
1161
  }
980
1162
  /**
@@ -987,8 +1169,10 @@ function regionSlotOf(value, region) {
987
1169
  * `value` may be the whole settings section or the `regions` map (see
988
1170
  * {@link regionsMapOf}).
989
1171
  */
990
- function regionEnabledOf(value, region) {
991
- 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";
992
1176
  }
993
1177
  /**
994
1178
  * Build the next `regions` settings value for a provider on/off toggle. ONLY
@@ -1006,9 +1190,9 @@ function regionEnabledOf(value, region) {
1006
1190
  * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1007
1191
  * 'regions', ...)` needs.
1008
1192
  */
1009
- function nextRegionEnabled(value, region, enabled) {
1010
- return nextRegionSlots(regionsMapOf(value), region, {
1011
- ...regionSlotOf(value, region),
1193
+ function nextRegionEnabled(value, panel, enabled) {
1194
+ return nextRegionSlots(regionsMapOf(value), panel, {
1195
+ ...regionSlotOf(value, panel),
1012
1196
  enabled
1013
1197
  });
1014
1198
  }
@@ -1025,8 +1209,8 @@ function nextRegionEnabled(value, region, enabled) {
1025
1209
  * `value` may be the whole settings section or the `regions` map (see
1026
1210
  * {@link regionsMapOf}).
1027
1211
  */
1028
- function regionCreditPositionOf(value, region) {
1029
- return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
1212
+ function regionCreditPositionOf(value, panel) {
1213
+ return regionSlotOf(value, panel)["creditPosition"] === "stats" ? "stats" : "composer";
1030
1214
  }
1031
1215
  /**
1032
1216
  * Build the next `regions` settings value for one region's credit seat.
@@ -1039,9 +1223,9 @@ function regionCreditPositionOf(value, region) {
1039
1223
  * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1040
1224
  * 'regions', ...)` needs.
1041
1225
  */
1042
- function nextRegionCreditPosition(value, region, position) {
1043
- return nextRegionSlots(regionsMapOf(value), region, {
1044
- ...regionSlotOf(value, region),
1226
+ function nextRegionCreditPosition(value, panel, position) {
1227
+ return nextRegionSlots(regionsMapOf(value), panel, {
1228
+ ...regionSlotOf(value, panel),
1045
1229
  creditPosition: position
1046
1230
  });
1047
1231
  }
@@ -1537,65 +1721,6 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
1537
1721
  return result;
1538
1722
  }
1539
1723
  //#endregion
1540
- //#region src/region.ts
1541
- /**
1542
- * Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
1543
- * The AI gateway is shared by both international installs (Trae desktop and
1544
- * TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
1545
- * answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
1546
- * single stable entry point.
1547
- */
1548
- const REGION_GATEWAYS = {
1549
- cn: {
1550
- chat: "https://trae-api-cn.mchost.guru",
1551
- remote: "https://solo.trae.cn/api/remote/v1",
1552
- pay: "https://api.trae.cn"
1553
- },
1554
- ai: {
1555
- chat: "https://coresg-normal.trae.ai",
1556
- remote: "https://coresg-normal.trae.ai/api/remote/v1",
1557
- pay: "https://growsg-normal.trae.ai"
1558
- }
1559
- };
1560
- /** Region for an edition label: the international installs belong to `ai`. */
1561
- function regionOfEdition(edition) {
1562
- return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
1563
- }
1564
- /**
1565
- * Region from the credential's `userRegion` claim. The desktop storage spells
1566
- * it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
1567
- * spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
1568
- */
1569
- function regionOfUserRegion(value) {
1570
- const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
1571
- if (typeof raw !== "string") return void 0;
1572
- const lowered = raw.trim().toLowerCase();
1573
- if (lowered === "cn") return "cn";
1574
- if (lowered === "sg" || lowered === "ai") return "ai";
1575
- }
1576
- /** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
1577
- function regionOfHost(host) {
1578
- if (host === void 0) return void 0;
1579
- const trimmed = host.trim();
1580
- if (trimmed === "") return void 0;
1581
- let hostname;
1582
- try {
1583
- hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
1584
- } catch {
1585
- return;
1586
- }
1587
- if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
1588
- if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
1589
- }
1590
- /**
1591
- * Region of a credential: the `userRegion` claim wins, the host suffix is the
1592
- * fallback, and the edition label is the last resort. Every level is derived
1593
- * from data the credential itself carries, so no user configuration is needed.
1594
- */
1595
- function regionOfCredential(credential) {
1596
- return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
1597
- }
1598
- //#endregion
1599
1724
  //#region src/auth.ts
1600
1725
  /** Host used for CLI tokens, which carry no host claim of their own. */
1601
1726
  const CLI_DEFAULT_HOST = "https://api.trae.cn";
@@ -1661,12 +1786,15 @@ function normalizeTraeCredential(raw, edition, source) {
1661
1786
  const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
1662
1787
  const refreshToken = optionalString(value["refreshToken"]);
1663
1788
  const userRegion = userRegionOf(value["userRegion"]);
1664
- 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);
1665
1792
  return {
1666
1793
  accessToken,
1667
1794
  ...refreshToken === void 0 ? {} : { refreshToken },
1668
1795
  userId: optionalString(value["userId"]) ?? "",
1669
1796
  ...accountName === void 0 ? {} : { accountName },
1797
+ ...tenantId === void 0 ? {} : { tenantId },
1670
1798
  host: optionalString(value["host"]) ?? "",
1671
1799
  ...userRegion === void 0 ? {} : { userRegion },
1672
1800
  expiresAtMs,
@@ -1675,6 +1803,18 @@ function normalizeTraeCredential(raw, edition, source) {
1675
1803
  source
1676
1804
  };
1677
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
+ }
1678
1818
  function traeAccountId(credential) {
1679
1819
  const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
1680
1820
  return createHash("sha256").update(stable).digest("hex").slice(0, 24);
@@ -1691,7 +1831,10 @@ function credentialOfStored(stored) {
1691
1831
  userId: record["userId"],
1692
1832
  host: record["host"],
1693
1833
  userRegion: record["userRegion"],
1694
- 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
+ },
1695
1838
  expiredAt: record["expiresAtMs"],
1696
1839
  refreshExpiredAt: record["refreshExpiredAtMs"]
1697
1840
  }, edition, "dsh");
@@ -1746,6 +1889,8 @@ var TraeCredentialStore = class {
1746
1889
  edition;
1747
1890
  accountId;
1748
1891
  region;
1892
+ requireTenant;
1893
+ hideTenantAccounts;
1749
1894
  ownPathExplicit;
1750
1895
  legacyOwnPath;
1751
1896
  legacyOwnPathExplicit;
@@ -1757,6 +1902,8 @@ var TraeCredentialStore = class {
1757
1902
  this.edition = options.edition ?? "auto";
1758
1903
  this.accountId = options.accountId;
1759
1904
  this.region = options.region;
1905
+ this.requireTenant = options.requireTenant === true;
1906
+ this.hideTenantAccounts = options.hideTenantAccounts === true;
1760
1907
  this.ownPathExplicit = options.ownPath;
1761
1908
  this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
1762
1909
  this.legacyOwnPathExplicit = options.legacyOwnPath;
@@ -1840,7 +1987,9 @@ var TraeCredentialStore = class {
1840
1987
  * the plugin performed is never discarded by this preference.
1841
1988
  */
1842
1989
  preferred(credentials) {
1843
- 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];
1844
1993
  }
1845
1994
  /**
1846
1995
  * The account the Trae app is signed in to right now, when that differs from
@@ -1864,6 +2013,7 @@ var TraeCredentialStore = class {
1864
2013
  id: liveId,
1865
2014
  accountName: live.accountName ?? (live.userId || `${live.edition} account`),
1866
2015
  edition: live.edition,
2016
+ enterprise: isEnterpriseAccount(live),
1867
2017
  region: regionOfCredential(live),
1868
2018
  source: live.source,
1869
2019
  tokenExpiresAtMs: live.expiresAtMs,
@@ -1871,25 +2021,94 @@ var TraeCredentialStore = class {
1871
2021
  selected: false
1872
2022
  };
1873
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
+ }
1874
2041
  async accounts() {
1875
- const credentials = await this.readAll();
1876
- const selectedExists = this.accountId !== void 0 && credentials.some((credential) => traeAccountId(credential) === this.accountId);
1877
- 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);
1878
2058
  return credentials.map((credential) => ({
1879
2059
  id: traeAccountId(credential),
1880
2060
  accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
1881
2061
  edition: credential.edition,
2062
+ enterprise: isEnterpriseAccount(credential),
1882
2063
  region: regionOfCredential(credential),
1883
2064
  source: credential.source,
1884
2065
  tokenExpiresAtMs: credential.expiresAtMs,
1885
2066
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
1886
- selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
2067
+ selected: inUseId !== void 0 && traeAccountId(credential) === inUseId
1887
2068
  }));
1888
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
+ }
1889
2109
  async current() {
1890
- const credentials = await this.readAll();
1891
- if (this.accountId === void 0) return this.preferred(credentials);
1892
- return credentials.find((credential) => traeAccountId(credential) === this.accountId);
2110
+ const discovered = await this.readAll();
2111
+ return this.inUse(discovered, this.offered(discovered));
1893
2112
  }
1894
2113
  /**
1895
2114
  * The stored credential of one specific account, without changing which
@@ -2637,7 +2856,19 @@ function readBody(req) {
2637
2856
  });
2638
2857
  }
2639
2858
  function createTraeShim(options) {
2640
- 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");
2641
2872
  const sockets = /* @__PURE__ */ new Set();
2642
2873
  const server = createServer((req, res) => {
2643
2874
  handle(req, res);
@@ -2651,18 +2882,22 @@ function createTraeShim(options) {
2651
2882
  server.once("error", reject);
2652
2883
  });
2653
2884
  server.listen(0, "127.0.0.1");
2654
- function bearerOk(req) {
2885
+ /** Route whose secret matches the request's bearer, or `undefined`. */
2886
+ function routeOf(req) {
2655
2887
  const match = typeof req.headers.authorization === "string" ? /^Bearer\s+(.+)$/i.exec(req.headers.authorization.trim()) : null;
2656
- if (match === null) return false;
2888
+ if (match === null) return void 0;
2657
2889
  const actual = Buffer.from(match[1] ?? "");
2658
- const expected = Buffer.from(secret);
2659
- 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
+ }
2660
2894
  }
2661
2895
  async function handle(req, res) {
2662
2896
  try {
2663
2897
  if (!hostIsLoopback(req.headers.host)) return writeError(res, 403, "host_not_allowed", "Host must be loopback");
2664
2898
  if (!originIsLoopback(req.headers.origin)) return writeError(res, 403, "origin_not_allowed", "Origin must be loopback");
2665
- 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");
2666
2901
  const url = req.url ?? "/";
2667
2902
  if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) return writeJson(res, 200, { ok: true });
2668
2903
  if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) return writeJson(res, 200, {
@@ -2696,7 +2931,7 @@ function createTraeShim(options) {
2696
2931
  const abort = () => controller.abort();
2697
2932
  req.once("aborted", abort);
2698
2933
  req.socket.once("close", abort);
2699
- const result = await options.client.chatStream(raw, controller.signal);
2934
+ const result = await route.client.chatStream(raw, controller.signal);
2700
2935
  if (!result.ok) return writeError(res, STATUS_BY_KIND[result.kind], result.kind, result.message);
2701
2936
  res.writeHead(200, {
2702
2937
  "Content-Type": "text/event-stream",
@@ -2732,6 +2967,12 @@ function createTraeShim(options) {
2732
2967
  return `http://127.0.0.1:${address.port}`;
2733
2968
  },
2734
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
+ },
2735
2976
  close: () => new Promise((resolve, reject) => {
2736
2977
  for (const socket of sockets) socket.destroy();
2737
2978
  server.close((error) => error === void 0 ? resolve() : reject(error));
@@ -3080,7 +3321,7 @@ var TraeSoloUpstreamClient = class {
3080
3321
  async fetchModels(signal) {
3081
3322
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
3082
3323
  const region = regionOfCredential(credential);
3083
- const base = this.options.baseUrl ?? REGION_GATEWAYS[region].chat;
3324
+ const base = this.options.baseUrl ?? this.chatBaseOf(credential);
3084
3325
  const headers = {
3085
3326
  ...buildTraeCnHeaders(credential, identity),
3086
3327
  Accept: "application/json"
@@ -3166,7 +3407,19 @@ var TraeSoloUpstreamClient = class {
3166
3407
  }
3167
3408
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
3168
3409
  const headers = buildTraeCnHeaders(credential, identity);
3169
- 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
+ }
3170
3423
  let response;
3171
3424
  try {
3172
3425
  response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
@@ -3183,10 +3436,36 @@ var TraeSoloUpstreamClient = class {
3183
3436
  message: `transport error: ${String(error)}`
3184
3437
  };
3185
3438
  }
3186
- if (response.ok) return {
3187
- ok: true,
3188
- response
3189
- };
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
+ }
3190
3469
  const text = (await response.text()).slice(0, 1024);
3191
3470
  this.options.log?.("dsh-connect-trae: llm_utils_chat rejected", {
3192
3471
  status: response.status,
@@ -3202,7 +3481,77 @@ var TraeSoloUpstreamClient = class {
3202
3481
  message: text || `Trae SOLO returned HTTP ${response.status}`
3203
3482
  };
3204
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
+ }
3205
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
+ }
3206
3555
  //#endregion
3207
3556
  //#region src/sse.ts
3208
3557
  /** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
@@ -3343,6 +3692,13 @@ function decodeTraeEvent(event) {
3343
3692
  * two different statements, and a user hits the second without touching the
3344
3693
  * first (issue #33).
3345
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.
3346
3702
  * - `2001` — the upstream has not published its function config for the route
3347
3703
  * that was asked. Evidence is issue #39: on a live CN account every SOLO
3348
3704
  * function's `get_detail_param` answered HTTP 200 with `config_info_list:
@@ -3360,7 +3716,8 @@ const TRAE_ERROR_HINTS = {
3360
3716
  4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
3361
3717
  4008: "Trae reports the account's SOLO quota is exhausted for this request",
3362
3718
  4011: "Trae throttled the SOLO channel for this account (a rate limit, not the credit balance)",
3363
- 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)"
3364
3721
  };
3365
3722
  /** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
3366
3723
  function traeErrorPlan(extra) {
@@ -3690,14 +4047,29 @@ var TraeSoloBridge = class {
3690
4047
  try {
3691
4048
  const input = JSON.parse(bodyJson);
3692
4049
  if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
3693
- 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);
3694
4052
  const fromCatalog = entry?.wireConfigName === void 0 ? void 0 : {
3695
4053
  configName: entry.wireConfigName,
3696
4054
  ...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
3697
4055
  };
3698
4056
  const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
3699
4057
  const target = fromCatalog ?? fromResolver;
3700
- 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;
3701
4073
  const wireFunction = target?.function ?? entry?.wireFunction;
3702
4074
  if (wireModel !== input["model"]) input["model"] = wireModel;
3703
4075
  if (wireFunction !== void 0 && input["function"] !== wireFunction) input["function"] = wireFunction;
@@ -5442,16 +5814,26 @@ function toCheckin(status) {
5442
5814
  };
5443
5815
  }
5444
5816
  /**
5445
- * Assemble one region's card document. `region` is the tab the card is on; the
5446
- * 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
5447
5819
  * document's model slots and account list are that region's by construction.
5448
5820
  * Sign-in state is read-only; credit is a live billing answer whose failure
5449
5821
  * degrades to `creditsError` rather than failing the whole document.
5450
5822
  */
5451
- async function traeWebUsage(deps, region) {
5452
- const store = deps.store(region);
5453
- const enabled = deps.regionEnabled(region);
5454
- 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);
5455
5837
  if ((await store.status()).state !== "signed-in") {
5456
5838
  const { failures } = await store.diagnose();
5457
5839
  return {
@@ -5480,26 +5862,26 @@ async function traeWebUsage(deps, region) {
5480
5862
  }
5481
5863
  let appLogin;
5482
5864
  try {
5483
- appLogin = await deps.liveLogin?.(region);
5865
+ appLogin = await deps.liveLogin?.(panel);
5484
5866
  } catch {}
5485
5867
  const account = {
5486
5868
  accountId: accounts.find((item) => item.selected)?.id ?? "",
5487
5869
  accountName: credential.accountName ?? credential.userId,
5488
5870
  tokenExpiresAtMs: credential.expiresAtMs,
5489
5871
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
5490
- region,
5872
+ region: regionOfPanel(panel),
5491
5873
  enabled,
5492
5874
  accounts,
5493
5875
  ...appLogin === void 0 ? {} : { appLogin },
5494
- models: deps.displayModels(region).map((model) => ({
5876
+ models: deps.displayModels(panel).map((model) => ({
5495
5877
  ...model,
5496
5878
  ...model.input === void 0 ? {} : { input: [...model.input] }
5497
5879
  })),
5498
- enabledModelIds: [...deps.enabledModelIds(region)],
5499
- ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(region) }
5880
+ enabledModelIds: [...deps.enabledModelIds(panel)],
5881
+ ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
5500
5882
  };
5501
- const client = deps.client(region);
5502
- if (region === "ai") try {
5883
+ const client = deps.client(panel);
5884
+ if (regionOfPanel(panel) === "ai") try {
5503
5885
  const payStatus = await client.payStatus();
5504
5886
  return {
5505
5887
  status: "signed-in",
@@ -5513,9 +5895,25 @@ async function traeWebUsage(deps, region) {
5513
5895
  payStatusError: safeMessage(error)
5514
5896
  };
5515
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
+ };
5516
5914
  const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), client.checkinStatus()]);
5517
5915
  const credits = snapshotResult.status === "fulfilled" ? toCredits(snapshotResult.value) : void 0;
5518
- 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;
5519
5917
  return {
5520
5918
  status: "signed-in",
5521
5919
  ...account,
@@ -5536,13 +5934,13 @@ async function traeWebUsage(deps, region) {
5536
5934
  * problem to explain. The list is capped so a machine with many stale
5537
5935
  * credentials cannot turn one status read into a burst of upstream calls.
5538
5936
  */
5539
- async function creditAlternatives(deps, region, accounts) {
5937
+ async function creditAlternatives(deps, panel, accounts) {
5540
5938
  if (deps.creditsOfAccount === void 0) return void 0;
5541
- 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);
5542
5940
  if (candidates.length === 0) return void 0;
5543
5941
  const alternatives = (await Promise.all(candidates.map(async (account) => {
5544
5942
  try {
5545
- const credits = await deps.creditsOfAccount(region, account.id);
5943
+ const credits = await deps.creditsOfAccount(panel, account.id);
5546
5944
  if (credits === void 0) return void 0;
5547
5945
  if (credits.workAvailable <= 0 && credits.generalAvailable <= 0) return void 0;
5548
5946
  return {
@@ -5559,11 +5957,11 @@ async function creditAlternatives(deps, region, accounts) {
5559
5957
  return alternatives.length === 0 ? void 0 : alternatives;
5560
5958
  }
5561
5959
  /**
5562
- * 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
5563
5961
  * domestic tab; an unknown value is refused rather than guessed.
5564
5962
  */
5565
5963
  /**
5566
- * The general balance of every account in one region.
5964
+ * The general balance of every account in one panel.
5567
5965
  *
5568
5966
  * One upstream read per account — the same call `creditsOfAccount` already
5569
5967
  * makes for the exhausted-balance alternatives, widened to every account and to
@@ -5574,16 +5972,16 @@ async function creditAlternatives(deps, region, accounts) {
5574
5972
  * figure: the table lists how many accounts exist even when one cannot be
5575
5973
  * quoted, rather than dropping it and making the switch unavailable.
5576
5974
  */
5577
- async function traeAccountCredits(deps, region) {
5578
- 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);
5579
5977
  if (accounts.length === 0) return [];
5580
5978
  return Promise.all(accounts.map(async (account) => {
5581
5979
  let generalAvailable;
5582
5980
  if (account.selected) try {
5583
- generalAvailable = toCredits(await deps.client(region).snapshot()).generalAvailable;
5981
+ generalAvailable = toCredits(await deps.client(panel).snapshot()).generalAvailable;
5584
5982
  } catch {}
5585
5983
  else if (deps.creditsOfAccount !== void 0) try {
5586
- generalAvailable = (await deps.creditsOfAccount(region, account.id))?.generalAvailable;
5984
+ generalAvailable = (await deps.creditsOfAccount(panel, account.id))?.generalAvailable;
5587
5985
  } catch {}
5588
5986
  return {
5589
5987
  id: account.id,
@@ -5594,12 +5992,12 @@ async function traeAccountCredits(deps, region) {
5594
5992
  }));
5595
5993
  }
5596
5994
  function requestRegion(req, res) {
5597
- const region = regionOfTraeStatusUrl(req.url ?? "/");
5598
- if (region === void 0) {
5995
+ const panel = regionOfTraeStatusUrl(req.url ?? "/");
5996
+ if (panel === void 0) {
5599
5997
  json(res, 400, { error: "unknown region" });
5600
5998
  return;
5601
5999
  }
5602
- return region;
6000
+ return panel;
5603
6001
  }
5604
6002
  /** Mount the GET usage route on an optional webServer context. */
5605
6003
  function registerTraeUsageRoute(ctx, deps) {
@@ -5616,17 +6014,17 @@ function registerTraeUsageRoute(ctx, deps) {
5616
6014
  json(res, 403, { error: "origin-not-trusted" });
5617
6015
  return;
5618
6016
  }
5619
- const region = requestRegion(req, res);
5620
- if (region === void 0) return;
6017
+ const panel = requestRegion(req, res);
6018
+ if (panel === void 0) return;
5621
6019
  try {
5622
- json(res, 200, await traeWebUsage(deps, region));
6020
+ json(res, 200, await traeWebUsage(deps, panel));
5623
6021
  } catch (error) {
5624
6022
  json(res, 500, { error: safeMessage(error) });
5625
6023
  }
5626
6024
  }
5627
6025
  });
5628
6026
  /**
5629
- * 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
5630
6028
  * composer panel is opened. One upstream read per account, paid once per
5631
6029
  * open rather than on the 5-minute cycle (see
5632
6030
  * {@link TRAE_ACCOUNT_CREDITS_PATH}).
@@ -5637,10 +6035,10 @@ function registerTraeUsageRoute(ctx, deps) {
5637
6035
  handler: async (req, res) => {
5638
6036
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
5639
6037
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5640
- const region = requestRegion(req, res);
5641
- if (region === void 0) return;
6038
+ const panel = requestRegion(req, res);
6039
+ if (panel === void 0) return;
5642
6040
  try {
5643
- json(res, 200, { accounts: await traeAccountCredits(deps, region) });
6041
+ json(res, 200, { accounts: await traeAccountCredits(deps, panel) });
5644
6042
  } catch (error) {
5645
6043
  json(res, 500, { error: safeMessage(error) });
5646
6044
  }
@@ -5660,12 +6058,12 @@ function registerTraeUsageRoute(ctx, deps) {
5660
6058
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
5661
6059
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5662
6060
  if (deps.accountStateOf === void 0) return json(res, 503, { error: "account status unavailable" });
5663
- const region = requestRegion(req, res);
5664
- if (region === void 0) return;
6061
+ const panel = requestRegion(req, res);
6062
+ if (panel === void 0) return;
5665
6063
  const accountId = new URL(req.url ?? "/", "http://localhost").searchParams.get("accountId");
5666
6064
  if (accountId === null || accountId.trim() === "") return json(res, 400, { error: "missing accountId" });
5667
6065
  try {
5668
- const state = await deps.accountStateOf(region, accountId);
6066
+ const state = await deps.accountStateOf(panel, accountId);
5669
6067
  if (state === void 0) return json(res, 404, { error: "unknown account" });
5670
6068
  json(res, 200, state);
5671
6069
  } catch (error) {
@@ -5679,10 +6077,10 @@ function registerTraeUsageRoute(ctx, deps) {
5679
6077
  handler: async (req, res) => {
5680
6078
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5681
6079
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5682
- const region = requestRegion(req, res);
5683
- if (region === void 0) return;
6080
+ const panel = requestRegion(req, res);
6081
+ if (panel === void 0) return;
5684
6082
  try {
5685
- 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) });
5686
6084
  } catch (error) {
5687
6085
  json(res, 500, { error: safeMessage(error) });
5688
6086
  }
@@ -5703,15 +6101,15 @@ function registerTraeUsageRoute(ctx, deps) {
5703
6101
  handler: async (req, res) => {
5704
6102
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5705
6103
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5706
- const region = requestRegion(req, res);
5707
- if (region === void 0) return;
6104
+ const panel = requestRegion(req, res);
6105
+ if (panel === void 0) return;
5708
6106
  const action = transferActionOf(req.url ?? "");
5709
6107
  if (action === void 0) return json(res, 400, { error: "unknown transfer action" });
5710
6108
  try {
5711
6109
  const body = await readTransferBody(req);
5712
6110
  if (action === "export") {
5713
6111
  const wanted = new Set(Array.isArray(body["accountIds"]) ? body["accountIds"].filter((id) => typeof id === "string") : []);
5714
- const store = deps.store(region);
6112
+ const store = deps.store(panel);
5715
6113
  const records = [];
5716
6114
  for (const account of await store.accounts()) {
5717
6115
  if (!wanted.has(account.id)) continue;
@@ -5738,8 +6136,8 @@ function registerTraeUsageRoute(ctx, deps) {
5738
6136
  const text = typeof body["text"] === "string" ? body["text"] : "";
5739
6137
  if (action === "preview") return json(res, 200, previewTransferFile(text));
5740
6138
  const indexes = Array.isArray(body["indexes"]) ? body["indexes"].filter((index) => typeof index === "number" && Number.isInteger(index)) : [];
5741
- const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, region);
5742
- 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()]);
5743
6141
  json(res, 200, {
5744
6142
  imported,
5745
6143
  skipped
@@ -5756,10 +6154,10 @@ function registerTraeUsageRoute(ctx, deps) {
5756
6154
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5757
6155
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5758
6156
  if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
5759
- const region = requestRegion(req, res);
5760
- if (region === void 0) return;
6157
+ const panel = requestRegion(req, res);
6158
+ if (panel === void 0) return;
5761
6159
  try {
5762
- json(res, 200, { models: await deps.discoverModels(region) });
6160
+ json(res, 200, { models: await deps.discoverModels(panel) });
5763
6161
  } catch (error) {
5764
6162
  json(res, 500, { error: safeMessage(error) });
5765
6163
  }
@@ -5782,8 +6180,8 @@ function registerTraeUsageRoute(ctx, deps) {
5782
6180
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5783
6181
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5784
6182
  if (deps.probeModels === void 0) return json(res, 503, { error: "model test unavailable" });
5785
- const region = requestRegion(req, res);
5786
- if (region === void 0) return;
6183
+ const panel = requestRegion(req, res);
6184
+ if (panel === void 0) return;
5787
6185
  let ids;
5788
6186
  try {
5789
6187
  ids = await readModelTestBody(req);
@@ -5793,7 +6191,7 @@ function registerTraeUsageRoute(ctx, deps) {
5793
6191
  if (ids.length === 0) return json(res, 400, { error: "no models to test" });
5794
6192
  if (ids.length > 64) return json(res, 400, { error: `too many models (limit ${String(64)})` });
5795
6193
  try {
5796
- json(res, 200, await deps.probeModels(region, ids));
6194
+ json(res, 200, await deps.probeModels(panel, ids));
5797
6195
  } catch (error) {
5798
6196
  json(res, 500, { error: safeMessage(error) });
5799
6197
  }
@@ -5825,9 +6223,9 @@ function registerTraeUsageRoute(ctx, deps) {
5825
6223
  handler: async (req, res) => {
5826
6224
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5827
6225
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5828
- const region = requestRegion(req, res);
5829
- if (region === void 0) return;
5830
- 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" });
5831
6229
  try {
5832
6230
  let accountId;
5833
6231
  try {
@@ -5837,7 +6235,7 @@ function registerTraeUsageRoute(ctx, deps) {
5837
6235
  }
5838
6236
  if (accountId !== void 0) {
5839
6237
  if (deps.claimCheckinOf === void 0) return json(res, 503, { error: "check-in by account unavailable" });
5840
- const outcome = await deps.claimCheckinOf(region, accountId);
6238
+ const outcome = await deps.claimCheckinOf(panel, accountId);
5841
6239
  if (outcome === void 0) return json(res, 404, { error: "unknown account" });
5842
6240
  const { claim, checkin: after } = outcome;
5843
6241
  const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
@@ -5850,7 +6248,7 @@ function registerTraeUsageRoute(ctx, deps) {
5850
6248
  checkin: toCheckin(after)
5851
6249
  });
5852
6250
  }
5853
- const client = deps.client(region);
6251
+ const client = deps.client(panel);
5854
6252
  const current = await client.checkinStatus();
5855
6253
  if (!current.enabled) return json(res, 409, { error: "check-in is not enabled for this account" });
5856
6254
  if (current.checkedIn) return json(res, 200, {
@@ -6119,18 +6517,24 @@ function settingsNamespaceOf(ctx) {
6119
6517
  return typeof id === "string" && id !== "" ? id : TRAE_SETTINGS_NS;
6120
6518
  }
6121
6519
  /**
6122
- * 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
6123
6521
  * the flat fields: those were always captured from the CN endpoint (the
6124
6522
  * plugin had no international support), so they are read as the CN state and
6125
6523
  * only when no explicit CN slot exists. The ai region never inherits them —
6126
6524
  * that inheritance is exactly the bug where a stale CN directory would be
6127
6525
  * intersected with the international catalog and silently drop the user's
6128
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.
6129
6533
  */
6130
- function regionStateOf(config, region) {
6131
- const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[region]);
6534
+ function regionStateOf(config, panel) {
6535
+ const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[panel]);
6132
6536
  if (stored !== void 0) return stored;
6133
- if (region !== "cn") return {};
6537
+ if (panel !== "cn") return {};
6134
6538
  return {
6135
6539
  ...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
6136
6540
  ...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
@@ -6139,15 +6543,23 @@ function regionStateOf(config, region) {
6139
6543
  };
6140
6544
  }
6141
6545
  /**
6142
- * 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:
6143
6547
  * only an explicit `false` disables it, so every config written before this
6144
6548
  * switch existed — including the pre-region-split flat fields, which never
6145
6549
  * carry `enabled` — keeps both providers running exactly as before. The card
6146
6550
  * reads the same rule through `regionEnabledOf`, so the two halves can never
6147
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.
6148
6558
  */
6149
- function regionEnabled(config, region) {
6150
- 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";
6151
6563
  }
6152
6564
  /**
6153
6565
  * WHERE one region's credit readout sits (learned from workbuddy #62).
@@ -6189,10 +6601,16 @@ const modelConfig = z.object({
6189
6601
  input: z.array(z.union(["text", "image"])),
6190
6602
  creditMultiplier: z.number(),
6191
6603
  wireFunction: z.string(),
6604
+ wireConfigName: z.string(),
6192
6605
  multimodal: z.boolean()
6193
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
+ */
6194
6612
  const regionStateConfig = z.object({
6195
- 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)"),
6196
6614
  lastCatalog: z.array(modelConfig).default([]),
6197
6615
  enabledModelIds: z.array(z.string()).default([]),
6198
6616
  imageModelIds: z.array(z.string()).default([]),
@@ -6201,7 +6619,8 @@ const regionStateConfig = z.object({
6201
6619
  });
6202
6620
  const accountSelectionConfig = z.object({
6203
6621
  cn: z.string().description("Selected domestic (CN) account id (never a token)"),
6204
- 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)")
6205
6624
  });
6206
6625
  const Config = z.object({
6207
6626
  authFile: asVolatile(z.string().description("Optional Trae storage.json path override")),
@@ -6215,8 +6634,8 @@ const Config = z.object({
6215
6634
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
6216
6635
  showPointsInMainUi: asVolatile(z.boolean().default(false).description("在输入框工具栏显示积分读数「Trae CN · <数值>」(仅当选中的模型属于本插件时出现,可点击查看账号与积分明细)")),
6217
6636
  followAppLogin: asVolatile(z.boolean().default(false).description("跟随 Trae 桌面端当前登录账号换绑(打开后每次配置生效时重绑到 App 现在登录的账号)")),
6218
- accounts: asVolatile(accountSelectionConfig.description("Per-region account selections, keyed cn | ai")),
6219
- 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")),
6220
6639
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
6221
6640
  enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
6222
6641
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
@@ -6228,8 +6647,8 @@ const REGION_KEYS = ["cn", "ai"];
6228
6647
  function apply(ctx, config) {
6229
6648
  const settingsNs = settingsNamespaceOf(ctx);
6230
6649
  let current = () => config;
6231
- const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
6232
- 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 ?? []);
6233
6652
  /**
6234
6653
  * Per-region wire state. Each region's discovery owns its own maps: the CN
6235
6654
  * and international rosters overlap (and spell some ids differently), so a
@@ -6242,30 +6661,30 @@ function apply(ctx, config) {
6242
6661
  * `lastCatalog` / `models` / `enabledModelIds` that still lists it.
6243
6662
  */
6244
6663
  const wireState = {};
6245
- for (const region of REGION_KEYS) wireState[region] = {
6664
+ for (const panel of TRAE_PANELS) wireState[panel] = {
6246
6665
  callableKeys: /* @__PURE__ */ new Set(),
6247
6666
  resolved: false,
6248
6667
  byId: /* @__PURE__ */ new Map(),
6249
6668
  byName: /* @__PURE__ */ new Map()
6250
6669
  };
6251
- const dropDeadModels = (rows, region) => {
6252
- const wire = wireState[region];
6670
+ const dropDeadModels = (rows, panel) => {
6671
+ const wire = wireState[panel];
6253
6672
  if (!wire.resolved) return rows;
6254
6673
  return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
6255
6674
  };
6256
- const fallbackModels = (value, region) => applyImageSelection(fallbackModelsFor(region), imageSet(value, region));
6257
- const derive = (value, raw, region) => {
6258
- const selectedImages = imageSet(value, region);
6259
- const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, region)), selectedImages), enabledSet(value, region), regionStateOf(value, region).contextBudgets ?? {});
6260
- 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);
6261
6680
  };
6262
- const configuredModels = (value, region) => {
6263
- const state = regionStateOf(value, region);
6264
- 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);
6265
6684
  };
6266
- const displayModels = (value, region) => {
6267
- const state = regionStateOf(value, region);
6268
- 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));
6269
6688
  };
6270
6689
  /**
6271
6690
  * Legacy migration for the pre-split single `accountId`: its region is
@@ -6275,9 +6694,20 @@ function apply(ctx, config) {
6275
6694
  * inheriting a selection that belongs to the other side of the split.
6276
6695
  */
6277
6696
  let legacyAccountRegion;
6278
- const effectiveAccountFor = (region, value) => {
6279
- 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];
6280
6708
  if (explicit !== void 0) return explicit;
6709
+ const region = regionOfPanel(panel);
6710
+ if (panel === "enterprise") return void 0;
6281
6711
  return legacyAccountRegion === region ? value.accountId : void 0;
6282
6712
  };
6283
6713
  /**
@@ -6299,10 +6729,29 @@ function apply(ctx, config) {
6299
6729
  }
6300
6730
  }
6301
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
+ */
6302
6741
  const stacks = {};
6303
- 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";
6304
6753
  const catalog = new TraeCatalog(region);
6305
- const wire = wireState[region];
6754
+ const wire = wireState[panel];
6306
6755
  /**
6307
6756
  * Best-effort device identity for this region's installation. Prefer the
6308
6757
  * selected credential's own edition so an international account reads its
@@ -6355,6 +6804,7 @@ function apply(ctx, config) {
6355
6804
  };
6356
6805
  const store = new TraeCredentialStore({
6357
6806
  region,
6807
+ ...panel === "enterprise" ? { requireTenant: true } : { hideTenantAccounts: !tenantLocal },
6358
6808
  ...unwrapVolatile(config.authFile) === void 0 ? {} : { storagePath: unwrapVolatile(config.authFile) },
6359
6809
  edition: unwrapVolatile(config.edition) ?? "auto",
6360
6810
  refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
@@ -6362,19 +6812,65 @@ function apply(ctx, config) {
6362
6812
  const solo = new TraeSoloUpstreamClient({
6363
6813
  credential: () => store.resolve(),
6364
6814
  identity,
6815
+ ...panel === "enterprise" ? { route: "enterprise" } : {},
6365
6816
  log: (message, detail) => ctx.logger.warn(message, detail)
6366
6817
  });
6367
6818
  const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
6368
6819
  const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
6369
- const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
6370
- 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
+ }
6371
6860
  const shim = createTraeShim({
6372
6861
  catalog,
6373
- client: delegating,
6862
+ routes: shimRoutes,
6374
6863
  logger: ctx.logger
6375
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)];
6376
6872
  let rawDiagnostic = () => ({ state: "disabled" });
6377
- if (region === "cn") (async () => {
6873
+ if (panel === "cn") (async () => {
6378
6874
  try {
6379
6875
  const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
6380
6876
  const gateway = createTraeRawGateway({
@@ -6388,7 +6884,7 @@ function apply(ctx, config) {
6388
6884
  runtime: resolved.runtime
6389
6885
  }
6390
6886
  }),
6391
- solo: upstream,
6887
+ solo: new TraeSoloBridge(primary.client, catalog, wireResolver),
6392
6888
  endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
6393
6889
  edition: resolved.identity.edition,
6394
6890
  identity: {
@@ -6398,7 +6894,7 @@ function apply(ctx, config) {
6398
6894
  runtime: resolved.runtime,
6399
6895
  enabled: false
6400
6896
  });
6401
- delegating.replace(gateway.upstream);
6897
+ primary.upstream.replace(gateway.upstream);
6402
6898
  rawDiagnostic = () => gateway.diagnostic();
6403
6899
  ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
6404
6900
  } catch (error) {
@@ -6409,12 +6905,14 @@ function apply(ctx, config) {
6409
6905
  credential: () => store.resolve(),
6410
6906
  deviceId: async () => (await identity()).deviceId
6411
6907
  });
6412
- stacks[region] = {
6908
+ stacks[panel] = {
6413
6909
  region,
6414
6910
  store,
6415
6911
  catalog,
6416
6912
  shim,
6417
- delegating,
6913
+ lastDiscovery: latestDiscovery,
6914
+ delegating: routeClients[providerOfPanel(panel)]?.upstream,
6915
+ routeClients,
6418
6916
  usageClient,
6419
6917
  /**
6420
6918
  * Credit balance of one named account, WITHOUT rebinding the store
@@ -6483,6 +6981,8 @@ function apply(ctx, config) {
6483
6981
  wire.byId.set(model.id, target);
6484
6982
  wire.byName.set(model.name.trim().toLowerCase(), target);
6485
6983
  }
6984
+ latestDiscovery.length = 0;
6985
+ latestDiscovery.push(...merged);
6486
6986
  return merged;
6487
6987
  },
6488
6988
  /**
@@ -6505,7 +7005,7 @@ function apply(ctx, config) {
6505
7005
  const skipped = [];
6506
7006
  for (const id of ids) if (known.has(id.trim().toLowerCase())) testable.push(id);
6507
7007
  else skipped.push(id);
6508
- const results = await probeModelsSequentially((body, signal) => upstream.chatStream(body, signal), testable);
7008
+ const results = await probeModelsSequentially((body, signal) => primary.upstream.chatStream(body, signal), testable);
6509
7009
  return skipped.length === 0 ? { results } : {
6510
7010
  results,
6511
7011
  skipped
@@ -6521,9 +7021,10 @@ function apply(ctx, config) {
6521
7021
  * never moves from here — the card's hint and the follow switch below are
6522
7022
  * the only writers.
6523
7023
  */
6524
- const liveLoginOf = async (region) => {
7024
+ const liveLoginOf = async (panel) => {
7025
+ const region = regionOfPanel(panel);
6525
7026
  try {
6526
- const live = await stacks[region].store.liveLogin();
7027
+ const live = await stacks[panel].store.liveLogin();
6527
7028
  if (live === void 0 || live.region !== region) return void 0;
6528
7029
  return {
6529
7030
  id: live.id,
@@ -6547,30 +7048,30 @@ function apply(ctx, config) {
6547
7048
  * event (`loader/volatile-update`) is what re-runs `applySelection` with the
6548
7049
  * updated document — the same channel every card write already uses.
6549
7050
  */
6550
- const followAppLogin = async (region) => {
6551
- const live = await liveLoginOf(region);
7051
+ const followAppLogin = async (panel) => {
7052
+ const live = await liveLoginOf(panel);
6552
7053
  if (live === void 0 || writeSettings === void 0) return void 0;
6553
7054
  const configured = configuredAccountsOf(current());
6554
- if (configured[region] === live.id) return live;
7055
+ if (configured[panel] === live.id) return live;
6555
7056
  await writeSettings({ accounts: {
6556
7057
  ...configured,
6557
- [region]: live.id
7058
+ [panel]: live.id
6558
7059
  } });
6559
7060
  return live;
6560
7061
  };
6561
7062
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
6562
- store: (region) => stacks[region].store,
6563
- client: (region) => stacks[region].usageClient,
6564
- displayModels: (region) => displayModels(current(), region),
6565
- enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
6566
- regionEnabled: (region) => regionEnabled(current(), region),
6567
- discoverModels: (region, signal) => stacks[region].discoverModels(signal),
6568
- probeModels: (region, ids) => stacks[region].probeModels(ids),
6569
- rawDiagnostic: (region) => stacks[region].rawDiagnostic(),
6570
- creditsOfAccount: (region, accountId) => stacks[region].creditsOfAccount(accountId),
6571
- accountStateOf: (region, accountId) => stacks[region].accountStateOf(accountId),
6572
- claimCheckinOf: (region, accountId) => stacks[region].claimCheckinOf(accountId),
6573
- 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)
6574
7075
  }));
6575
7076
  /**
6576
7077
  * Live registration handles per region, filled once the shim is listening.
@@ -6582,8 +7083,18 @@ function apply(ctx, config) {
6582
7083
  * card tab alone would leave every model selectable.
6583
7084
  */
6584
7085
  const registration = {
6585
- cn: {},
6586
- 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;
6587
7098
  };
6588
7099
  /**
6589
7100
  * Publish each region's on/off state to the harness (issue #11). Both swaps
@@ -6595,21 +7106,53 @@ function apply(ctx, config) {
6595
7106
  * lands immediately.
6596
7107
  */
6597
7108
  const syncRegionRegistration = (value) => {
6598
- for (const region of REGION_KEYS) registration[region].adapter?.replace(regionEnabled(value, region) ? [TRAE_PROVIDERS[region]] : []);
6599
- registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
6600
- provider: TRAE_PROVIDERS[region],
6601
- 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)),
6602
7116
  settingsNs,
6603
7117
  settingsPath: [],
6604
7118
  declared: false
6605
7119
  })));
6606
7120
  };
6607
- /** 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. */
6608
7122
  const applySelection = (value) => {
6609
- for (const region of REGION_KEYS) {
6610
- const stack = stacks[region];
6611
- stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(region, value));
6612
- 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);
6613
7156
  stack.invalidateAdapter();
6614
7157
  }
6615
7158
  syncRegionRegistration(value);
@@ -6668,9 +7211,9 @@ function apply(ctx, config) {
6668
7211
  if (followInFlight || !followAppLoginEnabled(current())) return;
6669
7212
  followInFlight = true;
6670
7213
  try {
6671
- for (const region of REGION_KEYS) {
6672
- if (!regionEnabled(current(), region)) continue;
6673
- await followAppLogin(region);
7214
+ for (const panel of TRAE_PANELS) {
7215
+ if (!regionEnabled(current(), panel)) continue;
7216
+ await followAppLogin(panel);
6674
7217
  }
6675
7218
  } catch (error) {
6676
7219
  ctx.logger.warn("dsh-connect-trae: follow-app-login rebind skipped", error);
@@ -6682,77 +7225,78 @@ function apply(ctx, config) {
6682
7225
  let stopped = false;
6683
7226
  ctx.effect(() => () => {
6684
7227
  stopped = true;
6685
- for (const region of REGION_KEYS) stacks[region].shim.close();
7228
+ for (const panel of TRAE_PANELS) stacks[panel].shim.close();
6686
7229
  });
6687
- 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 () => {
6688
7231
  if (stopped) return;
6689
7232
  try {
6690
7233
  const piAi = piAiRuntimeInfo();
6691
7234
  ctx.logger.info(`dsh-connect-trae: pi-ai ${piAi.version ?? "unknown"} (${piAi.generation} generation)${piAi.inspected ? "" : " — not resolved locally, the host supplies it"}`);
6692
- for (const region of REGION_KEYS) {
6693
- 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;
6694
7240
  const trae = createTraeAdapter({
6695
7241
  shim: stack.shim,
6696
7242
  catalog: stack.catalog,
6697
- provider: TRAE_PROVIDERS[region],
6698
- displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
7243
+ provider,
7244
+ displayName: displayNameOfTraeProvider(provider),
7245
+ secret: entry.secret,
6699
7246
  resolveAttachments: () => ctx.get("attachments")
6700
7247
  });
6701
7248
  stack.invalidateAdapter = () => {
6702
7249
  trae.invalidate();
6703
7250
  };
6704
- 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
+ });
6705
7255
  }
6706
7256
  ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => {
6707
- const region = regionOfTraeProvider(request.provider ?? "");
6708
- if (region === void 0) return [];
6709
- if (!regionEnabled(current(), region)) return [];
7257
+ const panel = panelOfProvider(request.provider ?? "");
7258
+ if (panel === void 0) return [];
7259
+ if (!regionEnabled(current(), panel)) return [];
6710
7260
  const cancellation = signal ?? request.signal;
6711
- 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) => ({
6712
7264
  id: model.id,
6713
- name: traeModelDisplayName(model),
7265
+ name: traeModelDisplayName(model, withRate),
6714
7266
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
6715
7267
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
6716
7268
  inputModalities: traeInputModalities(model)
6717
7269
  }));
6718
7270
  });
6719
- registration.cn.directory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
6720
- provider: TRAE_PROVIDERS[region],
6721
- 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)),
6722
7274
  settingsNs,
6723
7275
  settingsPath: [],
6724
7276
  declared: false
6725
7277
  })));
6726
7278
  syncRegionRegistration(current());
6727
7279
  } finally {
6728
- if (registration.cn.adapter === void 0 || registration.ai.adapter === void 0 || registration.cn.directory === void 0) {
6729
- registration.cn.adapter?.();
6730
- registration.ai.adapter?.();
6731
- registration.cn.directory?.();
6732
- }
7280
+ if (registration.cn.adapters.length === 0 || registration.ai.adapters.length === 0 || registration.cn.directory === void 0) disposeRegistration();
6733
7281
  }
6734
7282
  try {
6735
7283
  ctx.effect(() => () => {
6736
- registration.cn.adapter?.();
6737
- registration.ai.adapter?.();
6738
- registration.cn.directory?.();
7284
+ disposeRegistration();
6739
7285
  });
6740
7286
  } catch {
6741
- registration.cn.adapter?.();
6742
- registration.ai.adapter?.();
6743
- registration.cn.directory?.();
7287
+ disposeRegistration();
6744
7288
  }
6745
- for (const region of REGION_KEYS) {
6746
- if (!regionEnabled(current(), region)) continue;
7289
+ for (const panel of TRAE_PANELS) {
7290
+ if (!regionEnabled(current(), panel)) continue;
6747
7291
  (async () => {
6748
- const stack = stacks[region];
7292
+ const stack = stacks[panel];
6749
7293
  try {
6750
7294
  const models = await stack.discoverModels();
6751
7295
  if (stopped) return;
6752
- stack.catalog.set(derive(current(), models, region));
7296
+ stack.catalog.set(derive(current(), models, panel));
6753
7297
  } catch (error) {
6754
- ctx.logger.warn(`dsh-connect-trae: live ${region} model directory unavailable; serving the configured catalog`, error);
6755
- 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));
6756
7300
  }
6757
7301
  stack.invalidateAdapter();
6758
7302
  })();
@@ -6762,4 +7306,4 @@ function apply(ctx, config) {
6762
7306
  });
6763
7307
  }
6764
7308
  //#endregion
6765
- 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 };