dsh-connect-trae 1.4.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ import { createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual
9
9
  import { dirname, join } from "node:path";
10
10
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
11
11
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
12
- import { cpus, homedir, release } from "node:os";
12
+ import { cpus, homedir, hostname, release } from "node:os";
13
13
  import { createServer } from "node:http";
14
14
  import { Readable } from "node:stream";
15
15
  import { execFile } from "node:child_process";
@@ -38,6 +38,52 @@ const FALLBACK_TRAE_MODELS = [
38
38
  name: "Kimi-K2.6"
39
39
  }
40
40
  ];
41
+ /**
42
+ * Bootstrap catalog for the international (ai) region, captured from the
43
+ * live `coresg-normal.trae.ai/api/remote/v1/models` directory on 2026-09-15
44
+ * (docs/INTL_SG_EVIDENCE.md §3). The two rosters barely overlap (the CN list
45
+ * has no Gemini/GPT/MiniMax entries), so an international account must never
46
+ * be seeded with the CN list. Like the CN fallback it is replaced by the live
47
+ * refresh; image input stays the user's explicit opt-in (`imageModelIds`).
48
+ */
49
+ const FALLBACK_TRAE_MODELS_AI = [
50
+ {
51
+ id: "gemini-3.1-pro",
52
+ name: "Gemini-3.1-Pro-Preview"
53
+ },
54
+ {
55
+ id: "gemini-3-flash-solo",
56
+ name: "Gemini-3-Flash-Preview"
57
+ },
58
+ {
59
+ id: "minimax-m3",
60
+ name: "MiniMax-M3"
61
+ },
62
+ {
63
+ id: "minimax-m2.7",
64
+ name: "MiniMax-M2.7"
65
+ },
66
+ {
67
+ id: "kimi-k2.5",
68
+ name: "Kimi-K2.5"
69
+ },
70
+ {
71
+ id: "gpt-5.4",
72
+ name: "GPT-5.4"
73
+ },
74
+ {
75
+ id: "gpt-5.2",
76
+ name: "GPT-5.2"
77
+ }
78
+ ];
79
+ /**
80
+ * Static fallback directory for a region. Each region keeps its own model
81
+ * slot in settings; the fallback must match the region so an account never
82
+ * shows the other region's roster.
83
+ */
84
+ function fallbackModelsFor(region) {
85
+ return region === "ai" ? FALLBACK_TRAE_MODELS_AI : FALLBACK_TRAE_MODELS;
86
+ }
41
87
  /** Exact DSH modalities for one catalog entry; absent metadata is text-only. */
42
88
  function traeInputModalities(model) {
43
89
  return [...model.input ?? ["text"]];
@@ -167,7 +213,15 @@ function deriveCatalog(catalog, enabled, budgets = {}) {
167
213
  return applyContextBudgets(enabled.size === 0 ? catalog : catalog.filter((model) => enabled.has(model.id)), budgets);
168
214
  }
169
215
  var TraeCatalog = class {
170
- models = FALLBACK_TRAE_MODELS;
216
+ models;
217
+ /**
218
+ * @param region Seeds the static fallback for this region; each region's
219
+ * provider must never serve the other region's roster before its first live
220
+ * refresh lands.
221
+ */
222
+ constructor(region = "cn") {
223
+ this.models = fallbackModelsFor(region);
224
+ }
171
225
  current() {
172
226
  return this.models;
173
227
  }
@@ -181,7 +235,28 @@ var TraeCatalog = class {
181
235
  };
182
236
  //#endregion
183
237
  //#region src/adapter.ts
238
+ /** Provider route this bundle owns for the domestic (CN) gateway. */
184
239
  const TRAE_PROVIDER = "trae";
240
+ /**
241
+ * Provider route this bundle owns for the international (trae.ai) gateway.
242
+ * Named `-global` to match the plugin family's convention (`workbuddy-global`);
243
+ * the internal region bucket stays `ai`, which is the gateway/protocol name.
244
+ */
245
+ const TRAE_AI_PROVIDER = "trae-global";
246
+ /** The provider id each region registers as. */
247
+ const TRAE_PROVIDERS = {
248
+ cn: TRAE_PROVIDER,
249
+ ai: TRAE_AI_PROVIDER
250
+ };
251
+ /** Human-readable provider names, shown in the DSH model picker. */
252
+ const TRAE_PROVIDER_DISPLAY_NAMES = {
253
+ cn: "Trae",
254
+ ai: "Trae Global"
255
+ };
256
+ /** Region a provider route id belongs to. */
257
+ function regionOfTraeProvider(provider) {
258
+ for (const [region, id] of Object.entries(TRAE_PROVIDERS)) if (id === provider) return region;
259
+ }
185
260
  const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
186
261
  const INERT_AUTH = {
187
262
  credentials: {
@@ -206,12 +281,12 @@ const REQUEST_IMAGE_BUDGETS = {
206
281
  requestImagePixelBudget: 4194304,
207
282
  requestImageMaxBytes: 1048576
208
283
  };
209
- function toPiModel(info, baseUrl) {
284
+ function toPiModel(info, baseUrl, providerId) {
210
285
  return {
211
286
  id: info.id,
212
287
  name: traeModelDisplayName(info),
213
288
  api: "openai-completions",
214
- provider: TRAE_PROVIDER,
289
+ provider: providerId,
215
290
  baseUrl,
216
291
  input: traeInputModalities(info),
217
292
  cost: {
@@ -236,11 +311,13 @@ function toPiModel(info, baseUrl) {
236
311
  };
237
312
  }
238
313
  function createTraeAdapter(options) {
239
- const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`));
314
+ const providerId = options.provider ?? "trae";
315
+ const providerName = options.displayName ?? "Trae";
316
+ const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId));
240
317
  const provider = {
241
318
  ...createProvider({
242
- id: TRAE_PROVIDER,
243
- name: "Trae",
319
+ id: providerId,
320
+ name: providerName,
244
321
  auth: { apiKey: {
245
322
  name: "Trae loopback secret",
246
323
  async resolve({ credential }) {
@@ -257,8 +334,8 @@ function createTraeAdapter(options) {
257
334
  getModels: () => buildModels()
258
335
  };
259
336
  const profile = {
260
- provider: TRAE_PROVIDER,
261
- displayName: "Trae",
337
+ provider: providerId,
338
+ displayName: providerName,
262
339
  streamIdleTimeoutMs: TRAE_STREAM_IDLE_TIMEOUT_MS,
263
340
  retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
264
341
  configuredMaxTokens: /* @__PURE__ */ new Map(),
@@ -266,7 +343,7 @@ function createTraeAdapter(options) {
266
343
  ...REQUEST_IMAGE_BUDGETS,
267
344
  piProvider: provider
268
345
  };
269
- let profiles = /* @__PURE__ */ new Map([[TRAE_PROVIDER, profile]]);
346
+ let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
270
347
  return {
271
348
  adapter: new PiAiAdapter({
272
349
  profiles: () => profiles,
@@ -275,7 +352,7 @@ function createTraeAdapter(options) {
275
352
  ...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
276
353
  }),
277
354
  invalidate() {
278
- profiles = /* @__PURE__ */ new Map([[TRAE_PROVIDER, profile]]);
355
+ profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
279
356
  }
280
357
  };
281
358
  }
@@ -734,12 +811,86 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
734
811
  return result;
735
812
  }
736
813
  //#endregion
814
+ //#region src/region.ts
815
+ /**
816
+ * Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
817
+ * The AI gateway is shared by both international installs (Trae desktop and
818
+ * TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
819
+ * answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
820
+ * single stable entry point.
821
+ */
822
+ const REGION_GATEWAYS = {
823
+ cn: {
824
+ chat: "https://trae-api-cn.mchost.guru",
825
+ remote: "https://solo.trae.cn/api/remote/v1",
826
+ pay: "https://api.trae.cn"
827
+ },
828
+ ai: {
829
+ chat: "https://coresg-normal.trae.ai",
830
+ remote: "https://coresg-normal.trae.ai/api/remote/v1",
831
+ pay: "https://growsg-normal.trae.ai"
832
+ }
833
+ };
834
+ /** Region for an edition label: the international installs belong to `ai`. */
835
+ function regionOfEdition(edition) {
836
+ return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
837
+ }
838
+ /**
839
+ * Region from the credential's `userRegion` claim. The desktop storage spells
840
+ * it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
841
+ * spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
842
+ */
843
+ function regionOfUserRegion(value) {
844
+ const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
845
+ if (typeof raw !== "string") return void 0;
846
+ const lowered = raw.trim().toLowerCase();
847
+ if (lowered === "cn") return "cn";
848
+ if (lowered === "sg" || lowered === "ai") return "ai";
849
+ }
850
+ /** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
851
+ function regionOfHost(host) {
852
+ if (host === void 0) return void 0;
853
+ const trimmed = host.trim();
854
+ if (trimmed === "") return void 0;
855
+ let hostname;
856
+ try {
857
+ hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
858
+ } catch {
859
+ return;
860
+ }
861
+ if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
862
+ if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
863
+ }
864
+ /**
865
+ * Region of a credential: the `userRegion` claim wins, the host suffix is the
866
+ * fallback, and the edition label is the last resort. Every level is derived
867
+ * from data the credential itself carries, so no user configuration is needed.
868
+ */
869
+ function regionOfCredential(credential) {
870
+ return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
871
+ }
872
+ //#endregion
737
873
  //#region src/auth.ts
738
874
  /** Host used for CLI tokens, which carry no host claim of their own. */
739
875
  const CLI_DEFAULT_HOST = "https://api.trae.cn";
740
876
  const OWN_VERSION = 1;
741
877
  const TRAE_AUTH_FILENAME = ".trae-auth.json";
742
- function traeOwnAuthPath() {
878
+ /** Prefix of the plugin-owned per-region credential copies. */
879
+ const TRAE_OWN_PREFIX = ".trae-auth";
880
+ /**
881
+ * Plugin-owned copy path for one region. Each region's store refreshes into
882
+ * its own file so two simultaneously signed-in regions never overwrite each
883
+ * other's refreshed token.
884
+ */
885
+ function traeOwnAuthPath(region) {
886
+ return join(resolveDshHome(), `${TRAE_OWN_PREFIX}.${region}.json`);
887
+ }
888
+ /**
889
+ * Pre-dual-provider single-copy path. Still read as a migration source (a
890
+ * legacy credential serves the region it belongs to until that region's own
891
+ * first refresh writes the per-region file), and removed by `logout`.
892
+ */
893
+ function legacyTraeOwnAuthPath() {
743
894
  return join(resolveDshHome(), TRAE_AUTH_FILENAME);
744
895
  }
745
896
  function optionalString(value) {
@@ -753,6 +904,11 @@ function timeToMs(value) {
753
904
  const parsed = Date.parse(value);
754
905
  return Number.isFinite(parsed) ? parsed : void 0;
755
906
  }
907
+ /** Extract the `userRegion.region` string from either on-disk shape. */
908
+ function userRegionOf(value) {
909
+ const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
910
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
911
+ }
756
912
  function normalizeTraeCredential(raw, edition, source) {
757
913
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
758
914
  const value = raw;
@@ -761,6 +917,7 @@ function normalizeTraeCredential(raw, edition, source) {
761
917
  const expiresAtMs = timeToMs(value["expiredAt"] ?? value["expiresAt"]) ?? 0;
762
918
  const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
763
919
  const refreshToken = optionalString(value["refreshToken"]);
920
+ const userRegion = userRegionOf(value["userRegion"]);
764
921
  const accountName = optionalString((typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0)?.["username"]);
765
922
  return {
766
923
  accessToken,
@@ -768,6 +925,7 @@ function normalizeTraeCredential(raw, edition, source) {
768
925
  userId: optionalString(value["userId"]) ?? "",
769
926
  ...accountName === void 0 ? {} : { accountName },
770
927
  host: optionalString(value["host"]) ?? "",
928
+ ...userRegion === void 0 ? {} : { userRegion },
771
929
  expiresAtMs,
772
930
  ...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
773
931
  edition,
@@ -790,6 +948,7 @@ function parseOwn(text) {
790
948
  refreshToken: stored["refreshToken"],
791
949
  userId: stored["userId"],
792
950
  host: stored["host"],
951
+ userRegion: stored["userRegion"],
793
952
  account: stored["accountName"] === void 0 ? void 0 : { username: stored["accountName"] },
794
953
  expiredAt: stored["expiresAtMs"],
795
954
  refreshExpiredAt: stored["refreshExpiresAtMs"]
@@ -802,7 +961,10 @@ var TraeCredentialStore = class {
802
961
  storagePathOverride;
803
962
  edition;
804
963
  accountId;
805
- ownPath;
964
+ region;
965
+ ownPathExplicit;
966
+ legacyOwnPath;
967
+ legacyOwnPathExplicit;
806
968
  refresh;
807
969
  refreshMarginMs;
808
970
  inflight;
@@ -810,10 +972,45 @@ var TraeCredentialStore = class {
810
972
  this.storagePathOverride = options.storagePath;
811
973
  this.edition = options.edition ?? "auto";
812
974
  this.accountId = options.accountId;
813
- this.ownPath = options.ownPath ?? traeOwnAuthPath();
975
+ this.region = options.region;
976
+ this.ownPathExplicit = options.ownPath;
977
+ this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
978
+ this.legacyOwnPathExplicit = options.legacyOwnPath;
814
979
  this.refresh = options.refresh;
815
980
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
816
981
  }
982
+ /** Whether a credential's own claim belongs to this store's region. */
983
+ matchesRegion(credential) {
984
+ return this.region === void 0 || regionOfCredential(credential) === this.region;
985
+ }
986
+ /**
987
+ * The path this store refreshes into: the per-region file for a
988
+ * region-scoped store, the legacy single file otherwise, or an explicitly
989
+ * injected path in tests.
990
+ */
991
+ ownAuthPath() {
992
+ if (this.ownPathExplicit !== void 0) return this.ownPathExplicit;
993
+ return this.region !== void 0 ? traeOwnAuthPath(this.region) : this.legacyOwnPath;
994
+ }
995
+ /**
996
+ * Every plugin-owned copy to read, most preferred first. A region-scoped
997
+ * store reads the legacy single copy as its migration source (readAll's
998
+ * region filter drops it when it carries the other region's credential); an
999
+ * unscoped store reads everything so diagnostics see both regions.
1000
+ *
1001
+ * With an explicitly injected own path the legacy source is read ONLY when
1002
+ * it was injected too — a test that pins one file must not accidentally see
1003
+ * the real machine's legacy copy.
1004
+ */
1005
+ ownCandidates() {
1006
+ if (this.ownPathExplicit !== void 0) return this.legacyOwnPathExplicit !== void 0 ? [this.ownPathExplicit, this.legacyOwnPathExplicit] : [this.ownPathExplicit];
1007
+ if (this.region !== void 0) return [traeOwnAuthPath(this.region), this.legacyOwnPath];
1008
+ return [
1009
+ this.legacyOwnPath,
1010
+ traeOwnAuthPath("cn"),
1011
+ traeOwnAuthPath("ai")
1012
+ ];
1013
+ }
817
1014
  setSource(storagePath, edition = "auto", accountId) {
818
1015
  this.storagePathOverride = storagePath;
819
1016
  this.edition = edition;
@@ -838,7 +1035,8 @@ var TraeCredentialStore = class {
838
1035
  }];
839
1036
  }
840
1037
  const all = traeStorageCandidates();
841
- return this.edition === "auto" ? all.filter((candidate) => candidate.edition === "cn" || candidate.edition === "solo") : all.filter((candidate) => candidate.edition === this.edition && (candidate.edition === "cn" || candidate.edition === "solo"));
1038
+ const cliEdition = "cn";
1039
+ return this.edition === "auto" ? all.filter((candidate) => candidate.source === "desktop" || candidate.edition === cliEdition) : all.filter((candidate) => candidate.edition === this.edition && (candidate.source === "desktop" || candidate.edition === cliEdition));
842
1040
  }
843
1041
  /**
844
1042
  * Deterministic default when no account is explicitly selected: the first
@@ -857,6 +1055,7 @@ var TraeCredentialStore = class {
857
1055
  id: traeAccountId(credential),
858
1056
  accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
859
1057
  edition: credential.edition,
1058
+ region: regionOfCredential(credential),
860
1059
  source: credential.source,
861
1060
  tokenExpiresAtMs: credential.expiresAtMs,
862
1061
  selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
@@ -895,16 +1094,34 @@ var TraeCredentialStore = class {
895
1094
  } catch {}
896
1095
  return false;
897
1096
  }
1097
+ /**
1098
+ * Remove every plugin-owned copy this store could read (per-region file,
1099
+ * legacy single file, and their lock siblings); the desktop storage files
1100
+ * are untouched. A region store's logout therefore also clears the legacy
1101
+ * migration source — deliberate: `logout` is the user's "forget what the
1102
+ * plugin stored" action, not a per-account toggle.
1103
+ */
898
1104
  async logout() {
899
- await rm(this.ownPath, { force: true });
900
- await rm(`${this.ownPath}.lock`, { force: true });
1105
+ for (const path of this.ownCandidates()) {
1106
+ await rm(path, { force: true });
1107
+ await rm(`${path}.lock`, { force: true });
1108
+ }
901
1109
  }
1110
+ /**
1111
+ * Every local credential of this store's region, deduplicated by account id.
1112
+ * A region-scoped store sees only its own region's credentials: the other
1113
+ * region's accounts are invisible to selection, refresh, and status alike,
1114
+ * which is what keeps the two regions' providers from cross-billing.
1115
+ */
902
1116
  async readAll() {
903
1117
  const { credentials: desktop } = await this.readDesktopAll();
904
- const own = await this.readOwn();
905
- const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
906
- if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
907
- return [...desktop, cnOwn];
1118
+ const credentials = [...desktop.filter((credential) => this.matchesRegion(credential))];
1119
+ for (const own of await this.readOwns()) {
1120
+ if (!this.matchesRegion(own)) continue;
1121
+ if (credentials.some((credential) => traeAccountId(credential) === traeAccountId(own))) continue;
1122
+ credentials.push(own);
1123
+ }
1124
+ return credentials;
908
1125
  }
909
1126
  /**
910
1127
  * Which paths were tried and why each one failed. Read-only and token-free:
@@ -949,13 +1166,14 @@ var TraeCredentialStore = class {
949
1166
  credentialFrom(candidate, text) {
950
1167
  let credential;
951
1168
  if (candidate.source === "cli") {
1169
+ if (regionOfEdition(candidate.edition) !== "cn") throw new Error(`Trae CLI tokens are only verified for the CN region; ${candidate.edition} CLI homes are not supported yet`);
952
1170
  const claims = parseTraeCliToken(text);
953
1171
  credential = normalizeTraeCredential({
954
1172
  token: claims.accessToken,
955
1173
  userId: claims.userId,
956
1174
  host: CLI_DEFAULT_HOST,
957
1175
  expiredAt: claims.expiresAtMs
958
- }, candidate.edition === "sg" ? "cn" : candidate.edition, "cli");
1176
+ }, candidate.edition, "cli");
959
1177
  } else credential = normalizeTraeCredential(parseTraeStorageDocument(text), candidate.edition, "desktop");
960
1178
  if (credential === void 0) throw new Error(`${candidate.source} candidate could not be normalized into a credential`);
961
1179
  return credential;
@@ -982,12 +1200,17 @@ var TraeCredentialStore = class {
982
1200
  failures
983
1201
  };
984
1202
  }
985
- async readOwn() {
986
- try {
987
- return parseOwn(await readFile(this.ownPath, "utf8"));
988
- } catch {
989
- return;
990
- }
1203
+ /**
1204
+ * Every readable plugin-owned copy, in candidate order; absent or corrupt
1205
+ * files are skipped rather than propagated.
1206
+ */
1207
+ async readOwns() {
1208
+ const copies = [];
1209
+ for (const path of this.ownCandidates()) try {
1210
+ const parsed = parseOwn(await readFile(path, "utf8"));
1211
+ if (parsed !== void 0) copies.push(parsed);
1212
+ } catch {}
1213
+ return copies;
991
1214
  }
992
1215
  async refreshNow(credential) {
993
1216
  if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
@@ -1005,8 +1228,9 @@ var TraeCredentialStore = class {
1005
1228
  ...outcome.host === void 0 ? {} : { host: outcome.host },
1006
1229
  source: "dsh"
1007
1230
  };
1008
- await withFileLock(this.ownPath, async () => {
1009
- await writeFileAtomic(this.ownPath, `${JSON.stringify({
1231
+ const ownPath = this.ownAuthPath();
1232
+ await withFileLock(ownPath, async () => {
1233
+ await writeFileAtomic(ownPath, `${JSON.stringify({
1010
1234
  version: OWN_VERSION,
1011
1235
  credential: refreshed
1012
1236
  }, null, 2)}\n`, {
@@ -1023,29 +1247,60 @@ var TraeCredentialStore = class {
1023
1247
  };
1024
1248
  //#endregion
1025
1249
  //#region src/refresh.ts
1026
- const CLIENT_ID_BY_EDITION = {
1027
- cn: "ono9krqynydwx5",
1028
- solo: "ono9krqynydwx5"
1250
+ const REFRESH_CONTRACT = {
1251
+ cn: {
1252
+ path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
1253
+ clientId: "ono9krqynydwx5",
1254
+ deviceInfo: false
1255
+ },
1256
+ sg: {
1257
+ path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
1258
+ clientId: "ono9krqynydwx5",
1259
+ deviceInfo: false
1260
+ },
1261
+ solo: {
1262
+ path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
1263
+ clientId: "ono9krqynydwx5",
1264
+ deviceInfo: false
1265
+ },
1266
+ "solo-sg": {
1267
+ path: "/trae/api/v3/oauth/ExchangeToken",
1268
+ clientId: "en1oxy7wnw8j9n",
1269
+ deviceInfo: true
1270
+ }
1029
1271
  };
1030
1272
  function normalizeHost(host) {
1031
1273
  const value = host.trim();
1032
1274
  if (value === "") throw new Error("Trae refresh host is missing");
1033
1275
  return value.replace(/\/$/, "");
1034
1276
  }
1035
- /** Refresh only editions whose client contract is currently evidenced. */
1036
- async function refreshTraeCredential(credential, signal) {
1037
- const clientId = CLIENT_ID_BY_EDITION[credential.edition];
1038
- if (clientId === void 0) throw new Error(`Trae ${credential.edition} refresh contract is not verified`);
1277
+ /**
1278
+ * Exchange a refresh token for a fresh access token, following the calling
1279
+ * edition's verified contract. `device` is only used by editions whose
1280
+ * official client sends a DeviceInfo body; when it cannot be resolved the
1281
+ * field is omitted rather than sent empty.
1282
+ */
1283
+ async function refreshTraeCredential(credential, signal, device) {
1284
+ const contract = REFRESH_CONTRACT[credential.edition];
1285
+ if (contract === void 0) throw new Error(`Trae ${credential.edition} refresh contract is not verified`);
1039
1286
  if (credential.refreshToken === void 0) throw new Error("Trae refresh token is missing");
1040
- const response = await fetch(`${normalizeHost(credential.host)}/cloudide/api/v3/trae/oauth/ExchangeToken`, {
1287
+ const body = {
1288
+ ClientID: contract.clientId,
1289
+ ClientSecret: "-",
1290
+ RefreshToken: credential.refreshToken,
1291
+ UserID: credential.userId
1292
+ };
1293
+ if (contract.deviceInfo && device !== void 0) body["DeviceInfo"] = {
1294
+ DeviceID: device.deviceId,
1295
+ MachineID: device.machineId,
1296
+ PlatformCode: credential.edition === "solo-sg" ? "SOLO_PC" : "TRAE",
1297
+ DeviceType: "PC",
1298
+ DeviceName: hostname()
1299
+ };
1300
+ const response = await fetch(`${normalizeHost(credential.host)}${contract.path}`, {
1041
1301
  method: "POST",
1042
1302
  headers: { "Content-Type": "application/json" },
1043
- body: JSON.stringify({
1044
- ClientID: clientId,
1045
- RefreshToken: credential.refreshToken,
1046
- ClientSecret: "-",
1047
- UserID: credential.userId
1048
- }),
1303
+ body: JSON.stringify(body),
1049
1304
  signal: signal ?? AbortSignal.timeout(3e4)
1050
1305
  });
1051
1306
  if (!response.ok) throw new Error(`Trae token refresh failed (http ${response.status})`);
@@ -1087,7 +1342,12 @@ async function readTraeIdentity(candidate, options = {}) {
1087
1342
  if (machineId === void 0) throw new Error(`Trae ${candidate.edition} has no stable machine identity`);
1088
1343
  const deviceId = dcDevice ?? devDevice ?? createHash("sha256").update(machineId).digest("hex").slice(0, 32);
1089
1344
  const buildVersion = nonEmpty(storage["iCubeLastVersion"]);
1090
- const appName = candidate.edition === "cn" ? "Trae CN" : candidate.edition === "solo" ? "TRAE SOLO CN" : void 0;
1345
+ const appName = {
1346
+ cn: "Trae CN",
1347
+ sg: "Trae",
1348
+ solo: "TRAE SOLO CN",
1349
+ "solo-sg": "TRAE SOLO"
1350
+ }[candidate.edition];
1091
1351
  const productPaths = [];
1092
1352
  if (appName !== void 0 && (platform === "darwin" || platform === "win32")) {
1093
1353
  if (platform === "darwin") productPaths.push(join("/Applications", `${appName}.app`, "Contents", "Resources", "app", "product.json"));
@@ -1381,8 +1641,13 @@ function normalizeTraeVersionCode(buildVersion) {
1381
1641
  const trimmed = buildVersion.trim();
1382
1642
  return /^\d+$/.test(trimmed) ? trimmed : TRAE_VERSION_CODE_FALLBACK;
1383
1643
  }
1384
- function buildTraeCnHeaders(credential, identity, options = {}) {
1385
- if (credential.edition !== "cn" && credential.edition !== "solo") throw new Error(`Trae ${credential.edition} request contract is not verified`);
1644
+ /**
1645
+ * Headers shared by every Trae edition. The same shape is accepted by both
1646
+ * gateways (verified read-only on the international gateway 2026-09-15,
1647
+ * docs/INTL_SG_EVIDENCE.md §2.3: identical `x-app-id`, identity headers, and
1648
+ * `Cloud-IDE-JWT` auth), so there is no per-edition branch any more.
1649
+ */
1650
+ function buildTraeHeaders(credential, identity, options = {}) {
1386
1651
  const requestId = options.requestId ?? randomUUID();
1387
1652
  const traceId = requestId.replaceAll("-", "").slice(0, 32);
1388
1653
  const profile = options.profile ?? "agent-task";
@@ -1426,6 +1691,8 @@ function buildTraeCnHeaders(credential, identity, options = {}) {
1426
1691
  "Accept": "text/event-stream"
1427
1692
  };
1428
1693
  }
1694
+ /** Backwards-compatible alias; the headers are no longer CN-specific. */
1695
+ const buildTraeCnHeaders = buildTraeHeaders;
1429
1696
  function traeEndpoint(baseUrl, path) {
1430
1697
  return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1431
1698
  }
@@ -1526,14 +1793,17 @@ var TraeSoloUpstreamClient = class {
1526
1793
  }
1527
1794
  async fetchModels(signal) {
1528
1795
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
1529
- const response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_SOLO_MODELS_PATH), {
1796
+ const region = regionOfCredential(credential);
1797
+ const base = this.options.baseUrl ?? REGION_GATEWAYS[region].chat;
1798
+ const directoryFunction = region === "ai" ? "solo_agent" : TRAE_SOLO_FUNCTION;
1799
+ const response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_MODELS_PATH), {
1530
1800
  method: "POST",
1531
1801
  headers: {
1532
1802
  ...buildTraeCnHeaders(credential, identity),
1533
1803
  Accept: "application/json"
1534
1804
  },
1535
1805
  body: JSON.stringify({
1536
- function: TRAE_SOLO_FUNCTION,
1806
+ function: directoryFunction,
1537
1807
  config_names: null,
1538
1808
  need_prompt: false,
1539
1809
  current_config_info: null,
@@ -1589,9 +1859,10 @@ var TraeSoloUpstreamClient = class {
1589
1859
  }
1590
1860
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
1591
1861
  const headers = buildTraeCnHeaders(credential, identity);
1862
+ const base = this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat;
1592
1863
  let response;
1593
1864
  try {
1594
- response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_SOLO_CHAT_PATH), {
1865
+ response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
1595
1866
  method: "POST",
1596
1867
  headers,
1597
1868
  body: prepared,
@@ -1960,6 +2231,24 @@ function parseTraeRemoteModel(value) {
1960
2231
  //#region src/solo-remote.ts
1961
2232
  const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
1962
2233
  /**
2234
+ * Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
2235
+ * CN locale headers; the international directory lives on the shared
2236
+ * `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
2237
+ * English/Singapore headers — both forms are accepted, each region keeps the
2238
+ * shape its own portal sends.
2239
+ */
2240
+ function remoteDressing(region) {
2241
+ return region === "ai" ? {
2242
+ referer: "https://coresg-normal.trae.ai/",
2243
+ timezone: "Asia/Singapore",
2244
+ language: "en"
2245
+ } : {
2246
+ referer: "https://solo.trae.cn/",
2247
+ timezone: "Asia/Shanghai",
2248
+ language: "zh-cn"
2249
+ };
2250
+ }
2251
+ /**
1963
2252
  * Read-only model catalog client for the SOLO Web API.
1964
2253
  *
1965
2254
  * This deliberately has no chat/session method: the Remote session protocol
@@ -1972,22 +2261,26 @@ var TraeSoloRemoteCatalogClient = class {
1972
2261
  constructor(options) {
1973
2262
  this.options = options;
1974
2263
  this.fetchImpl = options.fetchImpl ?? fetch;
1975
- this.baseUrl = options.baseUrl ?? "https://solo.trae.cn/api/remote/v1";
2264
+ this.baseUrl = options.baseUrl;
1976
2265
  }
1977
- async headers() {
2266
+ async headers(region) {
2267
+ const credential = await this.options.credential();
2268
+ const dressing = remoteDressing(region);
1978
2269
  return {
1979
- "Authorization": `Cloud-IDE-JWT ${(await this.options.credential()).accessToken}`,
2270
+ "Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
1980
2271
  "Content-Type": "application/json",
1981
2272
  "x-trae-client-type": "web",
1982
- "x-trae-user-timezone": "Asia/Shanghai",
1983
- "x-preferenced-language": "zh-cn",
1984
- "Referer": "https://solo.trae.cn/",
2273
+ "x-trae-user-timezone": dressing.timezone,
2274
+ "x-preferenced-language": dressing.language,
2275
+ "Referer": dressing.referer,
1985
2276
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1986
2277
  };
1987
2278
  }
1988
2279
  async fetchModels(signal) {
1989
- const headers = await this.headers();
1990
- const response = await this.fetchImpl(`${this.baseUrl}/models?functions=solo_agent_remote,solo_work_remote`, {
2280
+ const region = regionOfCredential(await this.options.credential());
2281
+ const base = this.baseUrl ?? REGION_GATEWAYS[region].remote;
2282
+ const headers = await this.headers(region);
2283
+ const response = await this.fetchImpl(`${base}/models?functions=solo_agent_remote,solo_work_remote`, {
1991
2284
  headers,
1992
2285
  signal: signal ?? AbortSignal.timeout(3e4)
1993
2286
  });
@@ -2164,8 +2457,9 @@ var TraeRawChatUpstreamClient = class {
2164
2457
  message: "messages are required"
2165
2458
  };
2166
2459
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
2460
+ const requestId = crypto.randomUUID();
2167
2461
  const headers = buildTraeCnHeaders(credential, identity, {
2168
- requestId: crypto.randomUUID(),
2462
+ requestId,
2169
2463
  profile: "raw-chat"
2170
2464
  });
2171
2465
  const runtime = this.options.config.runtime;
@@ -2190,7 +2484,7 @@ var TraeRawChatUpstreamClient = class {
2190
2484
  };
2191
2485
  let response;
2192
2486
  try {
2193
- response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_RAW_CHAT_V2_PATH), {
2487
+ response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat, TRAE_RAW_CHAT_V2_PATH), {
2194
2488
  method: "POST",
2195
2489
  headers,
2196
2490
  body: JSON.stringify(body),
@@ -2497,11 +2791,13 @@ var TraeDelegatingUpstreamClient = class {
2497
2791
  /**
2498
2792
  * Read-only Trae usage/credits client.
2499
2793
  *
2500
- * Sources the verified `api.trae.cn` pay/ug endpoints (see
2501
- * `docs/USAGE_API_RESEARCH.md`). All queries are read-only and do not consume
2502
- * Trae credits. The per-session consumption detail table is deliberately not
2503
- * included: the endpoint returns no rows for the current account, so we only
2504
- * expose what is actually retrievable.
2794
+ * CN sources the verified `api.trae.cn` pay/ug endpoints (see
2795
+ * `docs/USAGE_API_RESEARCH.md`); the international (ai) region is
2796
+ * subscription-based and reads `ide_user_pay_status` on its own gateway
2797
+ * (verified 2026-09-15, docs/INTL_SG_EVIDENCE.md §4). All queries are
2798
+ * read-only and do not consume Trae credits. The per-session consumption
2799
+ * detail table is deliberately not included: the endpoint returns no rows
2800
+ * for the current account, so we only expose what is actually retrievable.
2505
2801
  */
2506
2802
  const TRAE_PAY_BASE = "https://api.trae.cn";
2507
2803
  function asNumber(value) {
@@ -2559,23 +2855,37 @@ var TraeUsageClient = class {
2559
2855
  constructor(options) {
2560
2856
  this.options = options;
2561
2857
  this.fetchImpl = options.fetchImpl ?? fetch;
2562
- this.baseUrl = options.baseUrl ?? "https://api.trae.cn";
2858
+ this.baseUrl = options.baseUrl;
2563
2859
  this.timeoutMs = options.timeoutMs ?? 3e4;
2564
2860
  }
2861
+ /** Region of the current credential; `cn` when unresolvable. */
2862
+ async currentRegion() {
2863
+ const credential = await this.options.credential();
2864
+ return credential === void 0 ? "cn" : regionOfCredential(credential);
2865
+ }
2866
+ /**
2867
+ * Pay base for the current credential's region. An explicit baseUrl (tests,
2868
+ * diagnostics) pins the endpoint; otherwise CN uses `api.trae.cn` and the
2869
+ * international region its own verified pay gateway.
2870
+ */
2871
+ async payBase() {
2872
+ return this.baseUrl ?? REGION_GATEWAYS[await this.currentRegion()].pay;
2873
+ }
2565
2874
  async authedHeaders() {
2566
2875
  const credential = await this.options.credential();
2567
2876
  if (credential === void 0 || credential.accessToken === "") throw new Error("Trae credential is not available; cannot query usage");
2877
+ const origin = await this.currentRegion() === "ai" ? "https://www.trae.ai" : "https://www.trae.cn";
2568
2878
  return {
2569
2879
  "Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
2570
2880
  "Content-Type": "application/json",
2571
2881
  "User-Agent": "Mozilla/5.0",
2572
- "Origin": "https://www.trae.cn",
2573
- "Referer": "https://www.trae.cn/"
2882
+ "Origin": origin,
2883
+ "Referer": `${origin}/`
2574
2884
  };
2575
2885
  }
2576
2886
  async post(path, data, signal) {
2577
2887
  const headers = await this.authedHeaders();
2578
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
2888
+ const response = await this.fetchImpl(`${await this.payBase()}${path}`, {
2579
2889
  method: "POST",
2580
2890
  headers,
2581
2891
  body: JSON.stringify(data),
@@ -2584,12 +2894,53 @@ var TraeUsageClient = class {
2584
2894
  if (!response.ok) throw new Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
2585
2895
  return await response.json();
2586
2896
  }
2897
+ /**
2898
+ * Subscription/pay status of an international (ai) account. The CN region
2899
+ * never calls this (its contract is unverified there); the ai region uses
2900
+ * this instead of the Work-credit `snapshot`.
2901
+ */
2902
+ async payStatus(signal) {
2903
+ if (await this.currentRegion() !== "ai") throw new Error("Trae pay status is only available for the international (ai) region");
2904
+ const payload = await this.post("/trae/api/v1/pay/ide_user_pay_status", {}, signal);
2905
+ const flag = (key) => payload[key] === true;
2906
+ const trial = typeof payload["trial_status"] === "object" && payload["trial_status"] !== null ? payload["trial_status"] : {};
2907
+ const fissionStart = asNumber(payload["solo_fission_start_time"]);
2908
+ const fissionExpire = asNumber(payload["solo_fission_expire_time"]);
2909
+ const fissionMax = asNumber(payload["solo_fission_max_usage"]);
2910
+ return {
2911
+ isDollarUsageBilling: flag("is_dollar_usage_billing"),
2912
+ hasPackage: flag("has_package"),
2913
+ isPayFreshman: flag("is_pay_freshman") || flag("is_pay_freshman_v2"),
2914
+ inTrial: trial["is_in_trial"] === true,
2915
+ trialEndTimeMs: asNumber(trial["trial_end_time"]) ?? 0,
2916
+ enableSoloLite: flag("enable_solo_lite"),
2917
+ enableSoloBuilder: flag("enable_solo_builder"),
2918
+ enableSoloCoder: flag("enable_solo_coder"),
2919
+ enableSoloWeb: flag("enable_solo_web"),
2920
+ ...fissionStart === void 0 || fissionExpire === void 0 || fissionMax === void 0 ? {} : { fission: {
2921
+ startTimeMs: fissionStart,
2922
+ expireTimeMs: fissionExpire,
2923
+ maxUsage: fissionMax
2924
+ } }
2925
+ };
2926
+ }
2927
+ /**
2928
+ * The Work-credit endpoints are CN-only. The international region is
2929
+ * subscription-based and must read {@link payStatus} instead; guarding here
2930
+ * keeps the card's degraded `creditsError` message diagnosable rather than
2931
+ * letting an ai credential hit an unverified path on its pay gateway.
2932
+ */
2933
+ async requireCnRegion(method) {
2934
+ if (await this.currentRegion() !== "cn") throw new Error(`Trae ${method} is only available for the CN region; the international (ai) region uses payStatus`);
2935
+ }
2587
2936
  /** Total entitlements / credits and per-pack breakdown. */
2588
2937
  async snapshot(signal) {
2938
+ await this.requireCnRegion("usage snapshot");
2589
2939
  return parseUsageSnapshot(await this.post("/trae/api/v2/pay/web_user_ent_usage", { require_usage: true }, signal));
2590
2940
  }
2591
2941
  /** Daily check-in status. */
2592
2942
  async checkinStatus(signal) {
2943
+ await this.requireCnRegion("check-in status");
2593
2944
  const payload = await this.post("/trae/api/v2/ug/checkin_credits/status", {}, signal);
2594
2945
  return {
2595
2946
  checkedIn: payload["checked_in"] === true,
@@ -2599,6 +2950,7 @@ var TraeUsageClient = class {
2599
2950
  }
2600
2951
  /** Rewards / activity rules. */
2601
2952
  async activities(signal) {
2953
+ await this.requireCnRegion("activities");
2602
2954
  const payload = await this.post("/trae/api/v2/ug/activity/info", {}, signal);
2603
2955
  const rawActivities = Array.isArray(payload["commercial_activities"]) ? payload["commercial_activities"] : [];
2604
2956
  const activities = [];
@@ -2632,17 +2984,34 @@ var TraeUsageClient = class {
2632
2984
  };
2633
2985
  //#endregion
2634
2986
  //#region src/status-paths.ts
2635
- /**
2636
- * Node-free constants and types shared by the Host and browser halves.
2637
- * Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
2638
- * stays consistent with that project's external presentation.
2639
- */
2640
2987
  /** Plugin-owned usage endpoint consumed by its browser half. */
2641
2988
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
2642
2989
  /** Plugin-owned live model refresh endpoint. */
2643
2990
  const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
2644
2991
  /** Plugin-owned local account rescan endpoint. */
2645
2992
  const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
2993
+ /** Query parameter naming the region a card request addresses. */
2994
+ const TRAE_REGION_PARAM = "region";
2995
+ /** Every region, in card tab order. */
2996
+ const TRAE_REGIONS = ["cn", "ai"];
2997
+ /**
2998
+ * Address one region's status route. The two regions are separate provider
2999
+ * stacks; every card request carries the region whose tab the user is on.
3000
+ */
3001
+ function withTraeRegion(path, region) {
3002
+ return `${path}?${TRAE_REGION_PARAM}=${region}`;
3003
+ }
3004
+ /**
3005
+ * Read the region parameter off a status-route URL. Absent means the domestic
3006
+ * tab (`cn`); a present-but-unknown value returns undefined so the route can
3007
+ * answer 400 instead of guessing.
3008
+ */
3009
+ function regionOfTraeStatusUrl(url) {
3010
+ const at = url.indexOf("?");
3011
+ const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
3012
+ if (value === null || value === "") return "cn";
3013
+ return TRAE_REGIONS.includes(value) ? value : void 0;
3014
+ }
2646
3015
  //#endregion
2647
3016
  //#region src/web-status.ts
2648
3017
  /** Redact token-like content before it crosses to the browser. */
@@ -2688,14 +3057,17 @@ function toCredits(snapshot) {
2688
3057
  };
2689
3058
  }
2690
3059
  /**
2691
- * Assemble the card's usage document. Sign-in state is read-only; credit is a
2692
- * live billing answer whose failure degrades to `creditsError` rather than
2693
- * failing the whole document.
3060
+ * Assemble one region's card document. `region` is the tab the card is on; the
3061
+ * region-scoped store already answers with only that region's accounts, so the
3062
+ * document's model slots and account list are that region's by construction.
3063
+ * Sign-in state is read-only; credit is a live billing answer whose failure
3064
+ * degrades to `creditsError` rather than failing the whole document.
2694
3065
  */
2695
- async function traeWebUsage(deps) {
2696
- const accounts = await deps.store.accounts();
2697
- if ((await deps.store.status()).state !== "signed-in") {
2698
- const { failures } = await deps.store.diagnose();
3066
+ async function traeWebUsage(deps, region) {
3067
+ const store = deps.store(region);
3068
+ const accounts = await store.accounts();
3069
+ if ((await store.status()).state !== "signed-in") {
3070
+ const { failures } = await store.diagnose();
2699
3071
  return {
2700
3072
  status: "signed-out",
2701
3073
  accounts,
@@ -2710,7 +3082,7 @@ async function traeWebUsage(deps) {
2710
3082
  }
2711
3083
  let credential;
2712
3084
  try {
2713
- credential = await deps.store.resolve();
3085
+ credential = await store.resolve();
2714
3086
  } catch (error) {
2715
3087
  return {
2716
3088
  status: "signed-out",
@@ -2722,16 +3094,32 @@ async function traeWebUsage(deps) {
2722
3094
  accountId: accounts.find((item) => item.selected)?.id ?? "",
2723
3095
  accountName: credential.accountName ?? credential.userId,
2724
3096
  tokenExpiresAtMs: credential.expiresAtMs,
3097
+ region,
2725
3098
  accounts,
2726
- models: deps.displayModels().map((model) => ({
3099
+ models: deps.displayModels(region).map((model) => ({
2727
3100
  ...model,
2728
3101
  ...model.input === void 0 ? {} : { input: [...model.input] }
2729
3102
  })),
2730
- enabledModelIds: [...deps.enabledModelIds()],
2731
- ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic() }
3103
+ enabledModelIds: [...deps.enabledModelIds(region)],
3104
+ ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(region) }
2732
3105
  };
3106
+ const client = deps.client(region);
3107
+ if (region === "ai") try {
3108
+ const payStatus = await client.payStatus();
3109
+ return {
3110
+ status: "signed-in",
3111
+ ...account,
3112
+ payStatus
3113
+ };
3114
+ } catch (error) {
3115
+ return {
3116
+ status: "signed-in",
3117
+ ...account,
3118
+ payStatusError: safeMessage(error)
3119
+ };
3120
+ }
2733
3121
  try {
2734
- const snapshot = await deps.client.snapshot();
3122
+ const snapshot = await client.snapshot();
2735
3123
  return {
2736
3124
  status: "signed-in",
2737
3125
  ...account,
@@ -2745,6 +3133,18 @@ async function traeWebUsage(deps) {
2745
3133
  };
2746
3134
  }
2747
3135
  }
3136
+ /**
3137
+ * The region a request addresses, or a 400 answer. Absent parameter means the
3138
+ * domestic tab; an unknown value is refused rather than guessed.
3139
+ */
3140
+ function requestRegion(req, res) {
3141
+ const region = regionOfTraeStatusUrl(req.url ?? "/");
3142
+ if (region === void 0) {
3143
+ json(res, 400, { error: "unknown region" });
3144
+ return;
3145
+ }
3146
+ return region;
3147
+ }
2748
3148
  /** Mount the GET usage route on an optional webServer context. */
2749
3149
  function registerTraeUsageRoute(ctx, deps) {
2750
3150
  ctx.effect(() => {
@@ -2760,8 +3160,10 @@ function registerTraeUsageRoute(ctx, deps) {
2760
3160
  json(res, 403, { error: "origin-not-trusted" });
2761
3161
  return;
2762
3162
  }
3163
+ const region = requestRegion(req, res);
3164
+ if (region === void 0) return;
2763
3165
  try {
2764
- json(res, 200, await traeWebUsage(deps));
3166
+ json(res, 200, await traeWebUsage(deps, region));
2765
3167
  } catch (error) {
2766
3168
  json(res, 500, { error: safeMessage(error) });
2767
3169
  }
@@ -2773,8 +3175,10 @@ function registerTraeUsageRoute(ctx, deps) {
2773
3175
  handler: async (req, res) => {
2774
3176
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2775
3177
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
3178
+ const region = requestRegion(req, res);
3179
+ if (region === void 0) return;
2776
3180
  try {
2777
- json(res, 200, { accounts: await deps.store.accounts() });
3181
+ json(res, 200, { accounts: await deps.store(region).accounts() });
2778
3182
  } catch (error) {
2779
3183
  json(res, 500, { error: safeMessage(error) });
2780
3184
  }
@@ -2787,8 +3191,10 @@ function registerTraeUsageRoute(ctx, deps) {
2787
3191
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2788
3192
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
2789
3193
  if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
3194
+ const region = requestRegion(req, res);
3195
+ if (region === void 0) return;
2790
3196
  try {
2791
- json(res, 200, { models: await deps.discoverModels() });
3197
+ json(res, 200, { models: await deps.discoverModels(region) });
2792
3198
  } catch (error) {
2793
3199
  json(res, 500, { error: safeMessage(error) });
2794
3200
  }
@@ -2946,6 +3352,26 @@ const inject = ["llm"];
2946
3352
  * so no branding is needed for either host generation.
2947
3353
  */
2948
3354
  const TRAE_SETTINGS_NS = "trae";
3355
+ /**
3356
+ * One region's saved state. A config written before the region split has only
3357
+ * the flat fields: those were always captured from the CN endpoint (the
3358
+ * plugin had no international support), so they are read as the CN state and
3359
+ * only when no explicit CN slot exists. The ai region never inherits them —
3360
+ * that inheritance is exactly the bug where a stale CN directory would be
3361
+ * intersected with the international catalog and silently drop the user's
3362
+ * picks (the same failure workbuddy fixed with its region split).
3363
+ */
3364
+ function regionStateOf(config, region) {
3365
+ const stored = config.regions?.[region];
3366
+ if (stored !== void 0) return stored;
3367
+ if (region !== "cn") return {};
3368
+ return {
3369
+ ...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
3370
+ ...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
3371
+ ...config.imageModelIds === void 0 ? {} : { imageModelIds: config.imageModelIds },
3372
+ ...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
3373
+ };
3374
+ }
2949
3375
  const modelConfig = z.object({
2950
3376
  id: z.string().required(),
2951
3377
  name: z.string().required(),
@@ -2954,6 +3380,16 @@ const modelConfig = z.object({
2954
3380
  input: z.array(z.union(["text", "image"])),
2955
3381
  creditMultiplier: z.number()
2956
3382
  });
3383
+ const regionStateConfig = z.object({
3384
+ lastCatalog: z.array(modelConfig).default([]),
3385
+ enabledModelIds: z.array(z.string()).default([]),
3386
+ imageModelIds: z.array(z.string()).default([]),
3387
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({})
3388
+ });
3389
+ const accountSelectionConfig = z.object({
3390
+ cn: z.string().description("Selected domestic (CN) account id (never a token)"),
3391
+ ai: z.string().description("Selected international account id (never a token)")
3392
+ });
2957
3393
  const Config = z.object({
2958
3394
  authFile: z.string().description("Optional Trae storage.json path override"),
2959
3395
  edition: z.union([
@@ -2963,137 +3399,231 @@ const Config = z.object({
2963
3399
  "solo",
2964
3400
  "solo-sg"
2965
3401
  ]).default("auto").description("Trae edition hint"),
2966
- accountId: z.string().description("Selected local Trae account id (never a token)"),
2967
- lastCatalog: z.array(modelConfig).description("Last refreshed Trae raw model directory shown by the plugin card"),
2968
- enabledModelIds: z.array(z.string()).default([]).description("Trae model ids the user enabled"),
2969
- contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Local DSH context budget per Trae model"),
2970
- imageModelIds: z.array(z.string()).default([]).description("Trae model ids the user explicitly enabled for image input"),
3402
+ accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
3403
+ accounts: accountSelectionConfig.description("Per-region account selections, keyed cn | ai"),
3404
+ regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai"),
3405
+ lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
3406
+ enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
3407
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
3408
+ imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
2971
3409
  models: z.array(modelConfig).description("Legacy generated Trae model list")
2972
3410
  });
3411
+ /** Every region, in card tab order. */
3412
+ const REGION_KEYS = ["cn", "ai"];
2973
3413
  function apply(ctx, config) {
2974
- const catalog = new TraeCatalog();
2975
- const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
2976
- const imageSet = (value) => new Set(value.imageModelIds ?? []);
2977
- const callableKeys = /* @__PURE__ */ new Set();
2978
- let wireResolved = false;
2979
- const dropDeadModels = (rows) => {
2980
- if (!wireResolved) return rows;
2981
- return rows.filter((model) => callableKeys.has(model.id.trim().toLowerCase()) || callableKeys.has(model.name.trim().toLowerCase()));
3414
+ let current = () => config;
3415
+ const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
3416
+ const imageSet = (value, region) => new Set(regionStateOf(value, region).imageModelIds ?? []);
3417
+ /**
3418
+ * Per-region wire state. Each region's discovery owns its own maps: the CN
3419
+ * and international rosters overlap (and spell some ids differently), so a
3420
+ * shared map would let one region's answer filter the other region's
3421
+ * catalog. Display keys (lowercased id AND name) of every model known to be
3422
+ * callable via `llm_utils_chat` are recorded by that region's discovery; a
3423
+ * model whose id and name are both absent is a dead config_name (Remote
3424
+ * advertises it, `get_detail_param` has no match, e.g. `Doubao-Seed-Code` /
3425
+ * `glm-5.3`) and must never be served — even from a stale saved
3426
+ * `lastCatalog` / `models` / `enabledModelIds` that still lists it.
3427
+ */
3428
+ const wireState = {};
3429
+ for (const region of REGION_KEYS) wireState[region] = {
3430
+ callableKeys: /* @__PURE__ */ new Set(),
3431
+ resolved: false,
3432
+ byId: /* @__PURE__ */ new Map(),
3433
+ byName: /* @__PURE__ */ new Map()
2982
3434
  };
2983
- const fallbackModels = (value) => applyImageSelection(FALLBACK_TRAE_MODELS, imageSet(value));
2984
- const derive = (value, raw) => {
2985
- const selectedImages = imageSet(value);
2986
- const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw)), selectedImages), enabledSet(value), value.contextBudgets ?? {});
2987
- return derived.length > 0 ? derived : fallbackModels(value);
3435
+ const dropDeadModels = (rows, region) => {
3436
+ const wire = wireState[region];
3437
+ if (!wire.resolved) return rows;
3438
+ return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
2988
3439
  };
2989
- const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) : fallbackModels(value);
2990
- const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) : FALLBACK_TRAE_MODELS;
2991
- const store = new TraeCredentialStore({
2992
- ...config.authFile === void 0 ? {} : { storagePath: config.authFile },
2993
- edition: config.edition ?? "auto",
2994
- ...config.accountId === void 0 ? {} : { accountId: config.accountId },
2995
- refresh: (credential) => refreshTraeCredential(credential)
2996
- });
2997
- const identity = async () => {
2998
- const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
2999
- edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
3000
- path: config.authFile,
3001
- source: "desktop"
3002
- }];
3003
- if (candidates.length === 0) throw new Error("Trae storage was not found");
3004
- return pickTraeStorageIdentity(candidates);
3440
+ const fallbackModels = (value, region) => applyImageSelection(fallbackModelsFor(region), imageSet(value, region));
3441
+ const derive = (value, raw, region) => {
3442
+ const selectedImages = imageSet(value, region);
3443
+ const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, region)), selectedImages), enabledSet(value, region), regionStateOf(value, region).contextBudgets ?? {});
3444
+ return derived.length > 0 ? derived : fallbackModels(value, region);
3005
3445
  };
3006
- const solo = new TraeSoloUpstreamClient({
3007
- credential: () => store.resolve(),
3008
- identity,
3009
- baseUrl: "https://trae-api-cn.mchost.guru",
3010
- log: (message, detail) => ctx.logger.warn(message, detail)
3011
- });
3012
- const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
3013
- const wireById = /* @__PURE__ */ new Map();
3014
- const wireByName = /* @__PURE__ */ new Map();
3015
- const wireResolver = (displayId) => wireById.get(displayId) ?? wireByName.get(displayId.trim().toLowerCase());
3016
- const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
3017
- const delegating = new TraeDelegatingUpstreamClient(upstream);
3018
- const shim = createTraeShim({
3019
- catalog,
3020
- client: delegating,
3021
- logger: ctx.logger
3022
- });
3023
- let rawDiagnostic = () => ({ state: "disabled" });
3024
- (async () => {
3025
- try {
3026
- const { identity, runtime } = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
3027
- const gateway = createTraeRawGateway({
3028
- raw: new TraeRawChatUpstreamClient({
3029
- credential: () => store.resolve(),
3030
- identity: async () => identity,
3031
- config: {
3032
- model: runtime.modelName,
3033
- configName: runtime.configName,
3034
- passBackReasoning: true,
3035
- runtime
3036
- },
3037
- baseUrl: "https://trae-api-cn.mchost.guru"
3038
- }),
3039
- solo: upstream,
3040
- endpoint: "https://trae-api-cn.mchost.guru/api/ide/v2/llm_raw_chat",
3041
- edition: identity.edition,
3042
- identity: {
3043
- appVersion: identity.appVersion ?? "",
3044
- buildVersion: identity.buildVersion ?? ""
3045
- },
3046
- runtime,
3047
- enabled: false
3048
- });
3049
- delegating.replace(gateway.upstream);
3050
- rawDiagnostic = () => gateway.diagnostic();
3051
- ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
3052
- } catch (error) {
3053
- ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
3054
- }
3055
- })();
3056
- const usageClient = new TraeUsageClient({ credential: () => store.resolve() });
3057
- let current = () => config;
3058
- let invalidateAdapter = () => {};
3059
- const discoverModels = async (signal) => {
3060
- const [remote, wire] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
3061
- const merged = mergeTraeModelSources(remote, wire);
3062
- callableKeys.clear();
3063
- for (const model of merged) {
3064
- callableKeys.add(model.id.trim().toLowerCase());
3065
- callableKeys.add(model.name.trim().toLowerCase());
3066
- }
3067
- wireResolved = merged.length > 0;
3068
- wireById.clear();
3069
- wireByName.clear();
3070
- for (const model of merged) if (model.wireConfigName !== void 0) {
3071
- wireById.set(model.id, model.wireConfigName);
3072
- wireByName.set(model.name.trim().toLowerCase(), model.wireConfigName);
3073
- }
3074
- const next = applyImageSelection(merged, imageSet(current()));
3075
- catalog.set(next);
3076
- return merged;
3446
+ const configuredModels = (value, region) => {
3447
+ const state = regionStateOf(value, region);
3448
+ return state.lastCatalog?.length ? derive(value, state.lastCatalog, region) : region === "cn" && value.models?.length ? derive(value, value.models, region) : fallbackModels(value, region);
3077
3449
  };
3450
+ const displayModels = (value, region) => {
3451
+ const state = regionStateOf(value, region);
3452
+ return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), region) : region === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), region) : fallbackModelsFor(region);
3453
+ };
3454
+ /**
3455
+ * Legacy migration for the pre-split single `accountId`: its region is
3456
+ * resolved once from the local account scan and the selection is then
3457
+ * attributed to that region ONLY — the other region keeps its documented
3458
+ * default (first discovered account of that region) instead of silently
3459
+ * inheriting a selection that belongs to the other side of the split.
3460
+ */
3461
+ let legacyAccountRegion;
3462
+ const effectiveAccountFor = (region, value) => {
3463
+ const explicit = value.accounts?.[region];
3464
+ if (explicit !== void 0) return explicit;
3465
+ return legacyAccountRegion === region ? value.accountId : void 0;
3466
+ };
3467
+ const stacks = {};
3468
+ for (const region of REGION_KEYS) {
3469
+ const catalog = new TraeCatalog(region);
3470
+ const wire = wireState[region];
3471
+ /**
3472
+ * Best-effort device identity for this region's installation. Prefer the
3473
+ * selected credential's own edition so an international account reads its
3474
+ * own installation's identity (machine/device ids are per-install),
3475
+ * mirroring the credential store's account binding; the explicit `edition`
3476
+ * config still wins as the user's own narrowing. Candidates are then
3477
+ * narrowed to THIS region's editions, so the CN stack can never read an
3478
+ * international install's identity (and vice versa).
3479
+ */
3480
+ const identity = async () => {
3481
+ let preferred;
3482
+ try {
3483
+ preferred = (await store.current())?.edition;
3484
+ } catch {}
3485
+ const hint = (config.edition !== void 0 && config.edition !== "auto" ? config.edition : void 0) ?? preferred;
3486
+ const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (hint !== void 0 ? item.edition === hint : regionOfEdition(item.edition) === region)) : [{
3487
+ edition: hint ?? (region === "ai" ? "sg" : "solo"),
3488
+ path: config.authFile,
3489
+ source: "desktop"
3490
+ }];
3491
+ if (candidates.length === 0) throw new Error("Trae storage was not found");
3492
+ return pickTraeStorageIdentity(candidates);
3493
+ };
3494
+ /**
3495
+ * Resolution failures degrade to omitting DeviceInfo (its being required
3496
+ * is unverified, docs/INTL_SG_EVIDENCE.md §5) rather than failing the
3497
+ * refresh.
3498
+ */
3499
+ const refreshDeviceSafe = async () => {
3500
+ try {
3501
+ const value = await identity();
3502
+ return {
3503
+ deviceId: value.deviceId,
3504
+ machineId: value.machineId
3505
+ };
3506
+ } catch {
3507
+ return;
3508
+ }
3509
+ };
3510
+ const store = new TraeCredentialStore({
3511
+ region,
3512
+ ...config.authFile === void 0 ? {} : { storagePath: config.authFile },
3513
+ edition: config.edition ?? "auto",
3514
+ refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
3515
+ });
3516
+ const solo = new TraeSoloUpstreamClient({
3517
+ credential: () => store.resolve(),
3518
+ identity,
3519
+ log: (message, detail) => ctx.logger.warn(message, detail)
3520
+ });
3521
+ const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
3522
+ const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
3523
+ const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
3524
+ const delegating = new TraeDelegatingUpstreamClient(upstream);
3525
+ const shim = createTraeShim({
3526
+ catalog,
3527
+ client: delegating,
3528
+ logger: ctx.logger
3529
+ });
3530
+ let rawDiagnostic = () => ({ state: "disabled" });
3531
+ if (region === "cn") (async () => {
3532
+ try {
3533
+ const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
3534
+ const gateway = createTraeRawGateway({
3535
+ raw: new TraeRawChatUpstreamClient({
3536
+ credential: () => store.resolve(),
3537
+ identity: async () => resolved.identity,
3538
+ config: {
3539
+ model: resolved.runtime.modelName,
3540
+ configName: resolved.runtime.configName,
3541
+ passBackReasoning: true,
3542
+ runtime: resolved.runtime
3543
+ }
3544
+ }),
3545
+ solo: upstream,
3546
+ endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
3547
+ edition: resolved.identity.edition,
3548
+ identity: {
3549
+ appVersion: resolved.identity.appVersion ?? "",
3550
+ buildVersion: resolved.identity.buildVersion ?? ""
3551
+ },
3552
+ runtime: resolved.runtime,
3553
+ enabled: false
3554
+ });
3555
+ delegating.replace(gateway.upstream);
3556
+ rawDiagnostic = () => gateway.diagnostic();
3557
+ ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
3558
+ } catch (error) {
3559
+ ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
3560
+ }
3561
+ })();
3562
+ stacks[region] = {
3563
+ region,
3564
+ store,
3565
+ catalog,
3566
+ shim,
3567
+ delegating,
3568
+ usageClient: new TraeUsageClient({ credential: () => store.resolve() }),
3569
+ discoverModels: async (signal) => {
3570
+ const [remote, wireModels] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
3571
+ const merged = mergeTraeModelSources(remote, wireModels);
3572
+ wire.callableKeys.clear();
3573
+ for (const model of merged) {
3574
+ wire.callableKeys.add(model.id.trim().toLowerCase());
3575
+ wire.callableKeys.add(model.name.trim().toLowerCase());
3576
+ }
3577
+ wire.resolved = merged.length > 0;
3578
+ wire.byId.clear();
3579
+ wire.byName.clear();
3580
+ for (const model of merged) if (model.wireConfigName !== void 0) {
3581
+ wire.byId.set(model.id, model.wireConfigName);
3582
+ wire.byName.set(model.name.trim().toLowerCase(), model.wireConfigName);
3583
+ }
3584
+ return merged;
3585
+ },
3586
+ invalidateAdapter: () => {},
3587
+ rawDiagnostic: () => rawDiagnostic()
3588
+ };
3589
+ }
3078
3590
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
3079
- store,
3080
- client: usageClient,
3081
- displayModels: () => displayModels(current()),
3082
- enabledModelIds: () => current().enabledModelIds ?? [],
3083
- discoverModels,
3084
- rawDiagnostic: () => rawDiagnostic()
3591
+ store: (region) => stacks[region].store,
3592
+ client: (region) => stacks[region].usageClient,
3593
+ displayModels: (region) => displayModels(current(), region),
3594
+ enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
3595
+ discoverModels: (region, signal) => stacks[region].discoverModels(signal),
3596
+ rawDiagnostic: (region) => stacks[region].rawDiagnostic()
3085
3597
  }));
3598
+ /** Push the current config into every region's store selection and catalog. */
3599
+ const applySelection = (value) => {
3600
+ for (const region of REGION_KEYS) {
3601
+ const stack = stacks[region];
3602
+ stack.store.setSource(value.authFile, value.edition ?? "auto", effectiveAccountFor(region, value));
3603
+ stack.catalog.set(configuredModels(value, region));
3604
+ stack.invalidateAdapter();
3605
+ }
3606
+ };
3086
3607
  const sectionHooks = {
3087
3608
  setSource(source) {
3088
3609
  current = source;
3089
3610
  },
3090
3611
  onChange() {
3091
- const next = current();
3092
- store.setSource(next.authFile, next.edition ?? "auto", next.accountId);
3093
- catalog.set(configuredModels(next));
3094
- invalidateAdapter();
3612
+ applySelection(current());
3095
3613
  }
3096
3614
  };
3615
+ applySelection(config);
3616
+ (async () => {
3617
+ const id = current().accountId;
3618
+ if (id === void 0) return;
3619
+ try {
3620
+ for (const region of REGION_KEYS) if ((await stacks[region].store.accounts()).some((account) => account.id === id)) {
3621
+ legacyAccountRegion = region;
3622
+ applySelection(current());
3623
+ return;
3624
+ }
3625
+ } catch {}
3626
+ })();
3097
3627
  const legacyInstallSettingsSection = dshSettings.installSettingsSection;
3098
3628
  if (typeof legacyInstallSettingsSection === "function") legacyInstallSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
3099
3629
  else ctx.inject(["settings"], (settingsCtx) => {
@@ -3102,32 +3632,34 @@ function apply(ctx, config) {
3102
3632
  let stopped = false;
3103
3633
  ctx.effect(() => () => {
3104
3634
  stopped = true;
3105
- shim.close();
3635
+ for (const region of REGION_KEYS) stacks[region].shim.close();
3106
3636
  });
3107
- shim.ready.then(async () => {
3637
+ Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
3108
3638
  if (stopped) return;
3109
- try {
3110
- await discoverModels();
3111
- } catch (error) {
3112
- ctx.logger.warn("dsh-connect-trae: wire-id resolution failed at startup; falling back to display ids", error);
3113
- }
3114
- catalog.set(configuredModels(current()));
3115
- const trae = createTraeAdapter({
3116
- shim,
3117
- catalog,
3118
- resolveAttachments: () => ctx.get("attachments")
3119
- });
3120
- invalidateAdapter = () => {
3121
- trae.invalidate();
3122
- };
3123
- let releaseAdapter;
3639
+ let releaseAdapterCn;
3640
+ let releaseAdapterAi;
3124
3641
  let releaseDirectory;
3125
3642
  try {
3126
- releaseAdapter = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
3643
+ for (const region of REGION_KEYS) {
3644
+ const stack = stacks[region];
3645
+ const trae = createTraeAdapter({
3646
+ shim: stack.shim,
3647
+ catalog: stack.catalog,
3648
+ provider: TRAE_PROVIDERS[region],
3649
+ displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
3650
+ resolveAttachments: () => ctx.get("attachments")
3651
+ });
3652
+ stack.invalidateAdapter = () => {
3653
+ trae.invalidate();
3654
+ };
3655
+ if (region === "cn") releaseAdapterCn = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
3656
+ else releaseAdapterAi = ctx.llm.registerAdapter([TRAE_AI_PROVIDER], trae.adapter);
3657
+ }
3127
3658
  ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request, signal) => {
3128
- if (request.provider !== "trae") return [];
3659
+ const region = regionOfTraeProvider(request.provider ?? "");
3660
+ if (region === void 0) return [];
3129
3661
  const cancellation = signal ?? request.signal;
3130
- return applyImageSelection(await discoverModels(cancellation), imageSet(current())).map((model) => ({
3662
+ return applyImageSelection(await stacks[region].discoverModels(cancellation), imageSet(current(), region)).map((model) => ({
3131
3663
  id: model.id,
3132
3664
  name: traeModelDisplayName(model),
3133
3665
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
@@ -3135,31 +3667,46 @@ function apply(ctx, config) {
3135
3667
  inputModalities: traeInputModalities(model)
3136
3668
  }));
3137
3669
  });
3138
- releaseDirectory = ctx.llm.registerConfigurableProviders([{
3139
- provider: TRAE_PROVIDER,
3140
- displayName: "Trae",
3670
+ releaseDirectory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
3671
+ provider: TRAE_PROVIDERS[region],
3672
+ displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
3141
3673
  settingsNs: TRAE_SETTINGS_NS,
3142
3674
  settingsPath: [],
3143
3675
  declared: false
3144
- }]);
3676
+ })));
3145
3677
  } finally {
3146
- if (releaseAdapter === void 0 || releaseDirectory === void 0) {
3147
- releaseAdapter?.();
3678
+ if (releaseAdapterCn === void 0 || releaseAdapterAi === void 0 || releaseDirectory === void 0) {
3679
+ releaseAdapterCn?.();
3680
+ releaseAdapterAi?.();
3148
3681
  releaseDirectory?.();
3149
3682
  }
3150
3683
  }
3151
3684
  try {
3152
3685
  ctx.effect(() => () => {
3153
- releaseAdapter?.();
3686
+ releaseAdapterCn?.();
3687
+ releaseAdapterAi?.();
3154
3688
  releaseDirectory?.();
3155
3689
  });
3156
3690
  } catch {
3157
- releaseAdapter?.();
3691
+ releaseAdapterCn?.();
3692
+ releaseAdapterAi?.();
3158
3693
  releaseDirectory?.();
3159
3694
  }
3695
+ for (const region of REGION_KEYS) (async () => {
3696
+ const stack = stacks[region];
3697
+ try {
3698
+ const models = await stack.discoverModels();
3699
+ if (stopped) return;
3700
+ stack.catalog.set(derive(current(), models, region));
3701
+ } catch (error) {
3702
+ ctx.logger.warn(`dsh-connect-trae: live ${region} model directory unavailable; serving the configured catalog`, error);
3703
+ stack.catalog.set(configuredModels(current(), region));
3704
+ }
3705
+ stack.invalidateAdapter();
3706
+ })();
3160
3707
  }).catch((error) => {
3161
- ctx.logger.error("dsh-connect-trae: loopback shim failed; provider not registered", error);
3708
+ ctx.logger.error("dsh-connect-trae: loopback shim failed; providers not registered", error);
3162
3709
  });
3163
3710
  }
3164
3711
  //#endregion
3165
- export { Config, FALLBACK_TRAE_MODELS, SseDecoder, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, 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, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, hashTraeRawChatArg, identityHeaders, inject, mergeTraeModelSources, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeIdentity, refreshTraeCredential, registerTraeUsageRoute, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage };
3712
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, 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_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeIdentity, refreshTraeCredential, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, withTraeRegion };