dsh-connect-trae 1.4.2 → 2.0.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
@@ -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"));
@@ -1121,6 +1381,8 @@ async function readTraeIdentity(candidate, options = {}) {
1121
1381
  function isFileMissing(error) {
1122
1382
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1123
1383
  }
1384
+ /** Prefix of the "no candidate exists on disk" error; see {@link resolveTraeIdentity}. */
1385
+ const STORAGE_MISSING_PREFIX = "Trae storage was not found";
1124
1386
  /**
1125
1387
  * Try candidates in order and return the first that yields a valid identity;
1126
1388
  * fail hard only when none do. Mirrors the credential store's skip-missing
@@ -1139,9 +1401,96 @@ async function pickTraeStorageIdentity(candidates, options = {}) {
1139
1401
  if (!isFileMissing(error)) anyPresent = true;
1140
1402
  }
1141
1403
  const tried = candidates.map((item) => item.path).join(" or ");
1142
- if (!anyPresent) throw new Error(`Trae storage was not found (${tried})`);
1404
+ if (!anyPresent) throw new Error(`${STORAGE_MISSING_PREFIX} (${tried})`);
1143
1405
  throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Trae identity could not be resolved (${tried})`);
1144
1406
  }
1407
+ /** CLI dotfile home per edition; the CLI keeps its own home, not an Application Support entry. */
1408
+ const CLI_HOME_BY_EDITION = {
1409
+ cn: ".trae-cn",
1410
+ solo: ".trae-cn",
1411
+ sg: ".trae",
1412
+ "solo-sg": ".trae"
1413
+ };
1414
+ /**
1415
+ * Deterministic identity for a machine that only has the Trae CLI (`traecli`).
1416
+ *
1417
+ * A CLI-only machine has no desktop `storage.json`, so the desktop identity
1418
+ * reader has nothing to read — yet the request headers still need stable
1419
+ * machine/device ids. Everything here comes from identifiers the CLI itself
1420
+ * persists (never a per-request random value):
1421
+ *
1422
+ * - `argv.json`'s `crash-reporter-id` — a stable per-install UUID the CLI
1423
+ * writes on first run; used directly as the device id.
1424
+ * - `builtin/ide_version.json`'s `version` — the CLI build, sent as
1425
+ * `x-app-version` (the desktop reader gets this from `product.json`).
1426
+ * - a SHA-256 over the device id, host name and user name for `machineId`,
1427
+ * matching the 64-char hex shape the official clients send.
1428
+ *
1429
+ * When `crash-reporter-id` is absent the device id falls back to the same
1430
+ * hash (still deterministic). A machine with no CLI home at all keeps failing
1431
+ * loudly — a fabricated identity is worse than a visible "not signed in".
1432
+ */
1433
+ async function readTraeCliIdentity(edition, options = {}) {
1434
+ const platform = options.platform ?? process.platform;
1435
+ const home = options.home ?? homedir();
1436
+ const env = options.env ?? process.env;
1437
+ const cliHome = join(home, CLI_HOME_BY_EDITION[edition]);
1438
+ const argv = await readJsonFile(join(cliHome, "argv.json"));
1439
+ const version = await readJsonFile(join(cliHome, "builtin", "ide_version.json"));
1440
+ const crashReporterId = nonEmpty(argv?.["crash-reporter-id"]);
1441
+ const host = nonEmpty(env["HOSTNAME"]) ?? await readHostname() ?? "unknown-host";
1442
+ const user = nonEmpty(env["USER"]) ?? nonEmpty(env["USERNAME"]) ?? "unknown-user";
1443
+ const deviceId = crashReporterId ?? createHash("sha256").update(`trae-cli\0${host}\0${user}`).digest("hex").slice(0, 32);
1444
+ const machineId = createHash("sha256").update(`trae-cli-machine\0${deviceId}\0${host}`).digest("hex");
1445
+ const appVersion = nonEmpty(version?.["version"]);
1446
+ const deviceCpu = cpus()[0]?.model.split(" ")[0];
1447
+ const osVersion = `${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform} ${release()}`;
1448
+ return {
1449
+ edition,
1450
+ machineId,
1451
+ deviceId,
1452
+ ...appVersion === void 0 ? {} : { appVersion },
1453
+ ...deviceCpu === void 0 ? {} : { deviceCpu },
1454
+ osVersion,
1455
+ platform
1456
+ };
1457
+ }
1458
+ /** Read and parse a JSON file, tolerating absence and malformed content. */
1459
+ async function readJsonFile(path) {
1460
+ try {
1461
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1462
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
1463
+ } catch {
1464
+ return;
1465
+ }
1466
+ }
1467
+ /** OS host name, or undefined when unavailable. */
1468
+ async function readHostname() {
1469
+ try {
1470
+ const { hostname } = await import("node:os");
1471
+ return nonEmpty(hostname());
1472
+ } catch {
1473
+ return;
1474
+ }
1475
+ }
1476
+ /**
1477
+ * Resolve the request identity for one region: the desktop storage identity
1478
+ * when any desktop install exists, otherwise the CLI home's deterministic
1479
+ * identity (see {@link readTraeCliIdentity}).
1480
+ *
1481
+ * This is the seam the WSL2 / CLI-only case needs: `traecli` writes its token
1482
+ * to `~/.trae-cn/trae-jwt-token` and no `storage.json` exists anywhere, so
1483
+ * desktop-only resolution used to fail every directory refresh and chat
1484
+ * request even though the account itself was found.
1485
+ */
1486
+ async function resolveTraeIdentity(candidates, edition, options = {}) {
1487
+ try {
1488
+ return await pickTraeStorageIdentity(candidates, options);
1489
+ } catch (error) {
1490
+ if (!(error instanceof Error) || !error.message.startsWith(STORAGE_MISSING_PREFIX)) throw error;
1491
+ return readTraeCliIdentity(edition, options);
1492
+ }
1493
+ }
1145
1494
  /** Headers derived from actual persisted identity, never a new random identity per request. */
1146
1495
  function identityHeaders(identity) {
1147
1496
  return {
@@ -1381,8 +1730,13 @@ function normalizeTraeVersionCode(buildVersion) {
1381
1730
  const trimmed = buildVersion.trim();
1382
1731
  return /^\d+$/.test(trimmed) ? trimmed : TRAE_VERSION_CODE_FALLBACK;
1383
1732
  }
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`);
1733
+ /**
1734
+ * Headers shared by every Trae edition. The same shape is accepted by both
1735
+ * gateways (verified read-only on the international gateway 2026-09-15,
1736
+ * docs/INTL_SG_EVIDENCE.md §2.3: identical `x-app-id`, identity headers, and
1737
+ * `Cloud-IDE-JWT` auth), so there is no per-edition branch any more.
1738
+ */
1739
+ function buildTraeHeaders(credential, identity, options = {}) {
1386
1740
  const requestId = options.requestId ?? randomUUID();
1387
1741
  const traceId = requestId.replaceAll("-", "").slice(0, 32);
1388
1742
  const profile = options.profile ?? "agent-task";
@@ -1426,6 +1780,8 @@ function buildTraeCnHeaders(credential, identity, options = {}) {
1426
1780
  "Accept": "text/event-stream"
1427
1781
  };
1428
1782
  }
1783
+ /** Backwards-compatible alias; the headers are no longer CN-specific. */
1784
+ const buildTraeCnHeaders = buildTraeHeaders;
1429
1785
  function traeEndpoint(baseUrl, path) {
1430
1786
  return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1431
1787
  }
@@ -1526,14 +1882,17 @@ var TraeSoloUpstreamClient = class {
1526
1882
  }
1527
1883
  async fetchModels(signal) {
1528
1884
  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), {
1885
+ const region = regionOfCredential(credential);
1886
+ const base = this.options.baseUrl ?? REGION_GATEWAYS[region].chat;
1887
+ const directoryFunction = region === "ai" ? "solo_agent" : TRAE_SOLO_FUNCTION;
1888
+ const response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_MODELS_PATH), {
1530
1889
  method: "POST",
1531
1890
  headers: {
1532
1891
  ...buildTraeCnHeaders(credential, identity),
1533
1892
  Accept: "application/json"
1534
1893
  },
1535
1894
  body: JSON.stringify({
1536
- function: TRAE_SOLO_FUNCTION,
1895
+ function: directoryFunction,
1537
1896
  config_names: null,
1538
1897
  need_prompt: false,
1539
1898
  current_config_info: null,
@@ -1589,9 +1948,10 @@ var TraeSoloUpstreamClient = class {
1589
1948
  }
1590
1949
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
1591
1950
  const headers = buildTraeCnHeaders(credential, identity);
1951
+ const base = this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat;
1592
1952
  let response;
1593
1953
  try {
1594
- response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_SOLO_CHAT_PATH), {
1954
+ response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
1595
1955
  method: "POST",
1596
1956
  headers,
1597
1957
  body: prepared,
@@ -1960,6 +2320,24 @@ function parseTraeRemoteModel(value) {
1960
2320
  //#region src/solo-remote.ts
1961
2321
  const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
1962
2322
  /**
2323
+ * Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
2324
+ * CN locale headers; the international directory lives on the shared
2325
+ * `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
2326
+ * English/Singapore headers — both forms are accepted, each region keeps the
2327
+ * shape its own portal sends.
2328
+ */
2329
+ function remoteDressing(region) {
2330
+ return region === "ai" ? {
2331
+ referer: "https://coresg-normal.trae.ai/",
2332
+ timezone: "Asia/Singapore",
2333
+ language: "en"
2334
+ } : {
2335
+ referer: "https://solo.trae.cn/",
2336
+ timezone: "Asia/Shanghai",
2337
+ language: "zh-cn"
2338
+ };
2339
+ }
2340
+ /**
1963
2341
  * Read-only model catalog client for the SOLO Web API.
1964
2342
  *
1965
2343
  * This deliberately has no chat/session method: the Remote session protocol
@@ -1972,22 +2350,26 @@ var TraeSoloRemoteCatalogClient = class {
1972
2350
  constructor(options) {
1973
2351
  this.options = options;
1974
2352
  this.fetchImpl = options.fetchImpl ?? fetch;
1975
- this.baseUrl = options.baseUrl ?? "https://solo.trae.cn/api/remote/v1";
2353
+ this.baseUrl = options.baseUrl;
1976
2354
  }
1977
- async headers() {
2355
+ async headers(region) {
2356
+ const credential = await this.options.credential();
2357
+ const dressing = remoteDressing(region);
1978
2358
  return {
1979
- "Authorization": `Cloud-IDE-JWT ${(await this.options.credential()).accessToken}`,
2359
+ "Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
1980
2360
  "Content-Type": "application/json",
1981
2361
  "x-trae-client-type": "web",
1982
- "x-trae-user-timezone": "Asia/Shanghai",
1983
- "x-preferenced-language": "zh-cn",
1984
- "Referer": "https://solo.trae.cn/",
2362
+ "x-trae-user-timezone": dressing.timezone,
2363
+ "x-preferenced-language": dressing.language,
2364
+ "Referer": dressing.referer,
1985
2365
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1986
2366
  };
1987
2367
  }
1988
2368
  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`, {
2369
+ const region = regionOfCredential(await this.options.credential());
2370
+ const base = this.baseUrl ?? REGION_GATEWAYS[region].remote;
2371
+ const headers = await this.headers(region);
2372
+ const response = await this.fetchImpl(`${base}/models?functions=solo_agent_remote,solo_work_remote`, {
1991
2373
  headers,
1992
2374
  signal: signal ?? AbortSignal.timeout(3e4)
1993
2375
  });
@@ -2164,8 +2546,9 @@ var TraeRawChatUpstreamClient = class {
2164
2546
  message: "messages are required"
2165
2547
  };
2166
2548
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
2549
+ const requestId = crypto.randomUUID();
2167
2550
  const headers = buildTraeCnHeaders(credential, identity, {
2168
- requestId: crypto.randomUUID(),
2551
+ requestId,
2169
2552
  profile: "raw-chat"
2170
2553
  });
2171
2554
  const runtime = this.options.config.runtime;
@@ -2190,7 +2573,7 @@ var TraeRawChatUpstreamClient = class {
2190
2573
  };
2191
2574
  let response;
2192
2575
  try {
2193
- response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_RAW_CHAT_V2_PATH), {
2576
+ response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat, TRAE_RAW_CHAT_V2_PATH), {
2194
2577
  method: "POST",
2195
2578
  headers,
2196
2579
  body: JSON.stringify(body),
@@ -2497,11 +2880,13 @@ var TraeDelegatingUpstreamClient = class {
2497
2880
  /**
2498
2881
  * Read-only Trae usage/credits client.
2499
2882
  *
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.
2883
+ * CN sources the verified `api.trae.cn` pay/ug endpoints (see
2884
+ * `docs/USAGE_API_RESEARCH.md`); the international (ai) region is
2885
+ * subscription-based and reads `ide_user_pay_status` on its own gateway
2886
+ * (verified 2026-09-15, docs/INTL_SG_EVIDENCE.md §4). All queries are
2887
+ * read-only and do not consume Trae credits. The per-session consumption
2888
+ * detail table is deliberately not included: the endpoint returns no rows
2889
+ * for the current account, so we only expose what is actually retrievable.
2505
2890
  */
2506
2891
  const TRAE_PAY_BASE = "https://api.trae.cn";
2507
2892
  function asNumber(value) {
@@ -2559,23 +2944,37 @@ var TraeUsageClient = class {
2559
2944
  constructor(options) {
2560
2945
  this.options = options;
2561
2946
  this.fetchImpl = options.fetchImpl ?? fetch;
2562
- this.baseUrl = options.baseUrl ?? "https://api.trae.cn";
2947
+ this.baseUrl = options.baseUrl;
2563
2948
  this.timeoutMs = options.timeoutMs ?? 3e4;
2564
2949
  }
2950
+ /** Region of the current credential; `cn` when unresolvable. */
2951
+ async currentRegion() {
2952
+ const credential = await this.options.credential();
2953
+ return credential === void 0 ? "cn" : regionOfCredential(credential);
2954
+ }
2955
+ /**
2956
+ * Pay base for the current credential's region. An explicit baseUrl (tests,
2957
+ * diagnostics) pins the endpoint; otherwise CN uses `api.trae.cn` and the
2958
+ * international region its own verified pay gateway.
2959
+ */
2960
+ async payBase() {
2961
+ return this.baseUrl ?? REGION_GATEWAYS[await this.currentRegion()].pay;
2962
+ }
2565
2963
  async authedHeaders() {
2566
2964
  const credential = await this.options.credential();
2567
2965
  if (credential === void 0 || credential.accessToken === "") throw new Error("Trae credential is not available; cannot query usage");
2966
+ const origin = await this.currentRegion() === "ai" ? "https://www.trae.ai" : "https://www.trae.cn";
2568
2967
  return {
2569
2968
  "Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
2570
2969
  "Content-Type": "application/json",
2571
2970
  "User-Agent": "Mozilla/5.0",
2572
- "Origin": "https://www.trae.cn",
2573
- "Referer": "https://www.trae.cn/"
2971
+ "Origin": origin,
2972
+ "Referer": `${origin}/`
2574
2973
  };
2575
2974
  }
2576
2975
  async post(path, data, signal) {
2577
2976
  const headers = await this.authedHeaders();
2578
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
2977
+ const response = await this.fetchImpl(`${await this.payBase()}${path}`, {
2579
2978
  method: "POST",
2580
2979
  headers,
2581
2980
  body: JSON.stringify(data),
@@ -2584,12 +2983,53 @@ var TraeUsageClient = class {
2584
2983
  if (!response.ok) throw new Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
2585
2984
  return await response.json();
2586
2985
  }
2986
+ /**
2987
+ * Subscription/pay status of an international (ai) account. The CN region
2988
+ * never calls this (its contract is unverified there); the ai region uses
2989
+ * this instead of the Work-credit `snapshot`.
2990
+ */
2991
+ async payStatus(signal) {
2992
+ if (await this.currentRegion() !== "ai") throw new Error("Trae pay status is only available for the international (ai) region");
2993
+ const payload = await this.post("/trae/api/v1/pay/ide_user_pay_status", {}, signal);
2994
+ const flag = (key) => payload[key] === true;
2995
+ const trial = typeof payload["trial_status"] === "object" && payload["trial_status"] !== null ? payload["trial_status"] : {};
2996
+ const fissionStart = asNumber(payload["solo_fission_start_time"]);
2997
+ const fissionExpire = asNumber(payload["solo_fission_expire_time"]);
2998
+ const fissionMax = asNumber(payload["solo_fission_max_usage"]);
2999
+ return {
3000
+ isDollarUsageBilling: flag("is_dollar_usage_billing"),
3001
+ hasPackage: flag("has_package"),
3002
+ isPayFreshman: flag("is_pay_freshman") || flag("is_pay_freshman_v2"),
3003
+ inTrial: trial["is_in_trial"] === true,
3004
+ trialEndTimeMs: asNumber(trial["trial_end_time"]) ?? 0,
3005
+ enableSoloLite: flag("enable_solo_lite"),
3006
+ enableSoloBuilder: flag("enable_solo_builder"),
3007
+ enableSoloCoder: flag("enable_solo_coder"),
3008
+ enableSoloWeb: flag("enable_solo_web"),
3009
+ ...fissionStart === void 0 || fissionExpire === void 0 || fissionMax === void 0 ? {} : { fission: {
3010
+ startTimeMs: fissionStart,
3011
+ expireTimeMs: fissionExpire,
3012
+ maxUsage: fissionMax
3013
+ } }
3014
+ };
3015
+ }
3016
+ /**
3017
+ * The Work-credit endpoints are CN-only. The international region is
3018
+ * subscription-based and must read {@link payStatus} instead; guarding here
3019
+ * keeps the card's degraded `creditsError` message diagnosable rather than
3020
+ * letting an ai credential hit an unverified path on its pay gateway.
3021
+ */
3022
+ async requireCnRegion(method) {
3023
+ if (await this.currentRegion() !== "cn") throw new Error(`Trae ${method} is only available for the CN region; the international (ai) region uses payStatus`);
3024
+ }
2587
3025
  /** Total entitlements / credits and per-pack breakdown. */
2588
3026
  async snapshot(signal) {
3027
+ await this.requireCnRegion("usage snapshot");
2589
3028
  return parseUsageSnapshot(await this.post("/trae/api/v2/pay/web_user_ent_usage", { require_usage: true }, signal));
2590
3029
  }
2591
3030
  /** Daily check-in status. */
2592
3031
  async checkinStatus(signal) {
3032
+ await this.requireCnRegion("check-in status");
2593
3033
  const payload = await this.post("/trae/api/v2/ug/checkin_credits/status", {}, signal);
2594
3034
  return {
2595
3035
  checkedIn: payload["checked_in"] === true,
@@ -2599,6 +3039,7 @@ var TraeUsageClient = class {
2599
3039
  }
2600
3040
  /** Rewards / activity rules. */
2601
3041
  async activities(signal) {
3042
+ await this.requireCnRegion("activities");
2602
3043
  const payload = await this.post("/trae/api/v2/ug/activity/info", {}, signal);
2603
3044
  const rawActivities = Array.isArray(payload["commercial_activities"]) ? payload["commercial_activities"] : [];
2604
3045
  const activities = [];
@@ -2632,17 +3073,34 @@ var TraeUsageClient = class {
2632
3073
  };
2633
3074
  //#endregion
2634
3075
  //#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
3076
  /** Plugin-owned usage endpoint consumed by its browser half. */
2641
3077
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
2642
3078
  /** Plugin-owned live model refresh endpoint. */
2643
3079
  const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
2644
3080
  /** Plugin-owned local account rescan endpoint. */
2645
3081
  const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
3082
+ /** Query parameter naming the region a card request addresses. */
3083
+ const TRAE_REGION_PARAM = "region";
3084
+ /** Every region, in card tab order. */
3085
+ const TRAE_REGIONS = ["cn", "ai"];
3086
+ /**
3087
+ * Address one region's status route. The two regions are separate provider
3088
+ * stacks; every card request carries the region whose tab the user is on.
3089
+ */
3090
+ function withTraeRegion(path, region) {
3091
+ return `${path}?${TRAE_REGION_PARAM}=${region}`;
3092
+ }
3093
+ /**
3094
+ * Read the region parameter off a status-route URL. Absent means the domestic
3095
+ * tab (`cn`); a present-but-unknown value returns undefined so the route can
3096
+ * answer 400 instead of guessing.
3097
+ */
3098
+ function regionOfTraeStatusUrl(url) {
3099
+ const at = url.indexOf("?");
3100
+ const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
3101
+ if (value === null || value === "") return "cn";
3102
+ return TRAE_REGIONS.includes(value) ? value : void 0;
3103
+ }
2646
3104
  //#endregion
2647
3105
  //#region src/web-status.ts
2648
3106
  /** Redact token-like content before it crosses to the browser. */
@@ -2688,14 +3146,17 @@ function toCredits(snapshot) {
2688
3146
  };
2689
3147
  }
2690
3148
  /**
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.
3149
+ * Assemble one region's card document. `region` is the tab the card is on; the
3150
+ * region-scoped store already answers with only that region's accounts, so the
3151
+ * document's model slots and account list are that region's by construction.
3152
+ * Sign-in state is read-only; credit is a live billing answer whose failure
3153
+ * degrades to `creditsError` rather than failing the whole document.
2694
3154
  */
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();
3155
+ async function traeWebUsage(deps, region) {
3156
+ const store = deps.store(region);
3157
+ const accounts = await store.accounts();
3158
+ if ((await store.status()).state !== "signed-in") {
3159
+ const { failures } = await store.diagnose();
2699
3160
  return {
2700
3161
  status: "signed-out",
2701
3162
  accounts,
@@ -2710,7 +3171,7 @@ async function traeWebUsage(deps) {
2710
3171
  }
2711
3172
  let credential;
2712
3173
  try {
2713
- credential = await deps.store.resolve();
3174
+ credential = await store.resolve();
2714
3175
  } catch (error) {
2715
3176
  return {
2716
3177
  status: "signed-out",
@@ -2722,16 +3183,32 @@ async function traeWebUsage(deps) {
2722
3183
  accountId: accounts.find((item) => item.selected)?.id ?? "",
2723
3184
  accountName: credential.accountName ?? credential.userId,
2724
3185
  tokenExpiresAtMs: credential.expiresAtMs,
3186
+ region,
2725
3187
  accounts,
2726
- models: deps.displayModels().map((model) => ({
3188
+ models: deps.displayModels(region).map((model) => ({
2727
3189
  ...model,
2728
3190
  ...model.input === void 0 ? {} : { input: [...model.input] }
2729
3191
  })),
2730
- enabledModelIds: [...deps.enabledModelIds()],
2731
- ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic() }
3192
+ enabledModelIds: [...deps.enabledModelIds(region)],
3193
+ ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(region) }
2732
3194
  };
3195
+ const client = deps.client(region);
3196
+ if (region === "ai") try {
3197
+ const payStatus = await client.payStatus();
3198
+ return {
3199
+ status: "signed-in",
3200
+ ...account,
3201
+ payStatus
3202
+ };
3203
+ } catch (error) {
3204
+ return {
3205
+ status: "signed-in",
3206
+ ...account,
3207
+ payStatusError: safeMessage(error)
3208
+ };
3209
+ }
2733
3210
  try {
2734
- const snapshot = await deps.client.snapshot();
3211
+ const snapshot = await client.snapshot();
2735
3212
  return {
2736
3213
  status: "signed-in",
2737
3214
  ...account,
@@ -2745,6 +3222,18 @@ async function traeWebUsage(deps) {
2745
3222
  };
2746
3223
  }
2747
3224
  }
3225
+ /**
3226
+ * The region a request addresses, or a 400 answer. Absent parameter means the
3227
+ * domestic tab; an unknown value is refused rather than guessed.
3228
+ */
3229
+ function requestRegion(req, res) {
3230
+ const region = regionOfTraeStatusUrl(req.url ?? "/");
3231
+ if (region === void 0) {
3232
+ json(res, 400, { error: "unknown region" });
3233
+ return;
3234
+ }
3235
+ return region;
3236
+ }
2748
3237
  /** Mount the GET usage route on an optional webServer context. */
2749
3238
  function registerTraeUsageRoute(ctx, deps) {
2750
3239
  ctx.effect(() => {
@@ -2760,8 +3249,10 @@ function registerTraeUsageRoute(ctx, deps) {
2760
3249
  json(res, 403, { error: "origin-not-trusted" });
2761
3250
  return;
2762
3251
  }
3252
+ const region = requestRegion(req, res);
3253
+ if (region === void 0) return;
2763
3254
  try {
2764
- json(res, 200, await traeWebUsage(deps));
3255
+ json(res, 200, await traeWebUsage(deps, region));
2765
3256
  } catch (error) {
2766
3257
  json(res, 500, { error: safeMessage(error) });
2767
3258
  }
@@ -2773,8 +3264,10 @@ function registerTraeUsageRoute(ctx, deps) {
2773
3264
  handler: async (req, res) => {
2774
3265
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2775
3266
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
3267
+ const region = requestRegion(req, res);
3268
+ if (region === void 0) return;
2776
3269
  try {
2777
- json(res, 200, { accounts: await deps.store.accounts() });
3270
+ json(res, 200, { accounts: await deps.store(region).accounts() });
2778
3271
  } catch (error) {
2779
3272
  json(res, 500, { error: safeMessage(error) });
2780
3273
  }
@@ -2787,8 +3280,10 @@ function registerTraeUsageRoute(ctx, deps) {
2787
3280
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
2788
3281
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
2789
3282
  if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
3283
+ const region = requestRegion(req, res);
3284
+ if (region === void 0) return;
2790
3285
  try {
2791
- json(res, 200, { models: await deps.discoverModels() });
3286
+ json(res, 200, { models: await deps.discoverModels(region) });
2792
3287
  } catch (error) {
2793
3288
  json(res, 500, { error: safeMessage(error) });
2794
3289
  }
@@ -2946,6 +3441,26 @@ const inject = ["llm"];
2946
3441
  * so no branding is needed for either host generation.
2947
3442
  */
2948
3443
  const TRAE_SETTINGS_NS = "trae";
3444
+ /**
3445
+ * One region's saved state. A config written before the region split has only
3446
+ * the flat fields: those were always captured from the CN endpoint (the
3447
+ * plugin had no international support), so they are read as the CN state and
3448
+ * only when no explicit CN slot exists. The ai region never inherits them —
3449
+ * that inheritance is exactly the bug where a stale CN directory would be
3450
+ * intersected with the international catalog and silently drop the user's
3451
+ * picks (the same failure workbuddy fixed with its region split).
3452
+ */
3453
+ function regionStateOf(config, region) {
3454
+ const stored = config.regions?.[region];
3455
+ if (stored !== void 0) return stored;
3456
+ if (region !== "cn") return {};
3457
+ return {
3458
+ ...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
3459
+ ...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
3460
+ ...config.imageModelIds === void 0 ? {} : { imageModelIds: config.imageModelIds },
3461
+ ...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
3462
+ };
3463
+ }
2949
3464
  const modelConfig = z.object({
2950
3465
  id: z.string().required(),
2951
3466
  name: z.string().required(),
@@ -2954,6 +3469,16 @@ const modelConfig = z.object({
2954
3469
  input: z.array(z.union(["text", "image"])),
2955
3470
  creditMultiplier: z.number()
2956
3471
  });
3472
+ const regionStateConfig = z.object({
3473
+ lastCatalog: z.array(modelConfig).default([]),
3474
+ enabledModelIds: z.array(z.string()).default([]),
3475
+ imageModelIds: z.array(z.string()).default([]),
3476
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({})
3477
+ });
3478
+ const accountSelectionConfig = z.object({
3479
+ cn: z.string().description("Selected domestic (CN) account id (never a token)"),
3480
+ ai: z.string().description("Selected international account id (never a token)")
3481
+ });
2957
3482
  const Config = z.object({
2958
3483
  authFile: z.string().description("Optional Trae storage.json path override"),
2959
3484
  edition: z.union([
@@ -2963,137 +3488,229 @@ const Config = z.object({
2963
3488
  "solo",
2964
3489
  "solo-sg"
2965
3490
  ]).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"),
3491
+ accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
3492
+ accounts: accountSelectionConfig.description("Per-region account selections, keyed cn | ai"),
3493
+ regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai"),
3494
+ lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
3495
+ enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
3496
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
3497
+ imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
2971
3498
  models: z.array(modelConfig).description("Legacy generated Trae model list")
2972
3499
  });
3500
+ /** Every region, in card tab order. */
3501
+ const REGION_KEYS = ["cn", "ai"];
2973
3502
  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()));
3503
+ let current = () => config;
3504
+ const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
3505
+ const imageSet = (value, region) => new Set(regionStateOf(value, region).imageModelIds ?? []);
3506
+ /**
3507
+ * Per-region wire state. Each region's discovery owns its own maps: the CN
3508
+ * and international rosters overlap (and spell some ids differently), so a
3509
+ * shared map would let one region's answer filter the other region's
3510
+ * catalog. Display keys (lowercased id AND name) of every model known to be
3511
+ * callable via `llm_utils_chat` are recorded by that region's discovery; a
3512
+ * model whose id and name are both absent is a dead config_name (Remote
3513
+ * advertises it, `get_detail_param` has no match, e.g. `Doubao-Seed-Code` /
3514
+ * `glm-5.3`) and must never be served — even from a stale saved
3515
+ * `lastCatalog` / `models` / `enabledModelIds` that still lists it.
3516
+ */
3517
+ const wireState = {};
3518
+ for (const region of REGION_KEYS) wireState[region] = {
3519
+ callableKeys: /* @__PURE__ */ new Set(),
3520
+ resolved: false,
3521
+ byId: /* @__PURE__ */ new Map(),
3522
+ byName: /* @__PURE__ */ new Map()
2982
3523
  };
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);
3524
+ const dropDeadModels = (rows, region) => {
3525
+ const wire = wireState[region];
3526
+ if (!wire.resolved) return rows;
3527
+ return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
2988
3528
  };
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);
3529
+ const fallbackModels = (value, region) => applyImageSelection(fallbackModelsFor(region), imageSet(value, region));
3530
+ const derive = (value, raw, region) => {
3531
+ const selectedImages = imageSet(value, region);
3532
+ const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, region)), selectedImages), enabledSet(value, region), regionStateOf(value, region).contextBudgets ?? {});
3533
+ return derived.length > 0 ? derived : fallbackModels(value, region);
3005
3534
  };
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;
3535
+ const configuredModels = (value, region) => {
3536
+ const state = regionStateOf(value, region);
3537
+ return state.lastCatalog?.length ? derive(value, state.lastCatalog, region) : region === "cn" && value.models?.length ? derive(value, value.models, region) : fallbackModels(value, region);
3077
3538
  };
3539
+ const displayModels = (value, region) => {
3540
+ const state = regionStateOf(value, region);
3541
+ return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), region) : region === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), region) : fallbackModelsFor(region);
3542
+ };
3543
+ /**
3544
+ * Legacy migration for the pre-split single `accountId`: its region is
3545
+ * resolved once from the local account scan and the selection is then
3546
+ * attributed to that region ONLY — the other region keeps its documented
3547
+ * default (first discovered account of that region) instead of silently
3548
+ * inheriting a selection that belongs to the other side of the split.
3549
+ */
3550
+ let legacyAccountRegion;
3551
+ const effectiveAccountFor = (region, value) => {
3552
+ const explicit = value.accounts?.[region];
3553
+ if (explicit !== void 0) return explicit;
3554
+ return legacyAccountRegion === region ? value.accountId : void 0;
3555
+ };
3556
+ const stacks = {};
3557
+ for (const region of REGION_KEYS) {
3558
+ const catalog = new TraeCatalog(region);
3559
+ const wire = wireState[region];
3560
+ /**
3561
+ * Best-effort device identity for this region's installation. Prefer the
3562
+ * selected credential's own edition so an international account reads its
3563
+ * own installation's identity (machine/device ids are per-install),
3564
+ * mirroring the credential store's account binding; the explicit `edition`
3565
+ * config still wins as the user's own narrowing. Candidates are then
3566
+ * narrowed to THIS region's editions, so the CN stack can never read an
3567
+ * international install's identity (and vice versa).
3568
+ */
3569
+ const identity = async () => {
3570
+ let preferred;
3571
+ try {
3572
+ preferred = (await store.current())?.edition;
3573
+ } catch {}
3574
+ const hint = (config.edition !== void 0 && config.edition !== "auto" ? config.edition : void 0) ?? preferred;
3575
+ return resolveTraeIdentity(config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (hint !== void 0 ? item.edition === hint : regionOfEdition(item.edition) === region)) : [{
3576
+ edition: hint ?? (region === "ai" ? "sg" : "solo"),
3577
+ path: config.authFile,
3578
+ source: "desktop"
3579
+ }], hint ?? (region === "ai" ? "solo-sg" : "cn"));
3580
+ };
3581
+ /**
3582
+ * Resolution failures degrade to omitting DeviceInfo (its being required
3583
+ * is unverified, docs/INTL_SG_EVIDENCE.md §5) rather than failing the
3584
+ * refresh.
3585
+ */
3586
+ const refreshDeviceSafe = async () => {
3587
+ try {
3588
+ const value = await identity();
3589
+ return {
3590
+ deviceId: value.deviceId,
3591
+ machineId: value.machineId
3592
+ };
3593
+ } catch {
3594
+ return;
3595
+ }
3596
+ };
3597
+ const store = new TraeCredentialStore({
3598
+ region,
3599
+ ...config.authFile === void 0 ? {} : { storagePath: config.authFile },
3600
+ edition: config.edition ?? "auto",
3601
+ refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
3602
+ });
3603
+ const solo = new TraeSoloUpstreamClient({
3604
+ credential: () => store.resolve(),
3605
+ identity,
3606
+ log: (message, detail) => ctx.logger.warn(message, detail)
3607
+ });
3608
+ const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
3609
+ const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
3610
+ const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
3611
+ const delegating = new TraeDelegatingUpstreamClient(upstream);
3612
+ const shim = createTraeShim({
3613
+ catalog,
3614
+ client: delegating,
3615
+ logger: ctx.logger
3616
+ });
3617
+ let rawDiagnostic = () => ({ state: "disabled" });
3618
+ if (region === "cn") (async () => {
3619
+ try {
3620
+ const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
3621
+ const gateway = createTraeRawGateway({
3622
+ raw: new TraeRawChatUpstreamClient({
3623
+ credential: () => store.resolve(),
3624
+ identity: async () => resolved.identity,
3625
+ config: {
3626
+ model: resolved.runtime.modelName,
3627
+ configName: resolved.runtime.configName,
3628
+ passBackReasoning: true,
3629
+ runtime: resolved.runtime
3630
+ }
3631
+ }),
3632
+ solo: upstream,
3633
+ endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
3634
+ edition: resolved.identity.edition,
3635
+ identity: {
3636
+ appVersion: resolved.identity.appVersion ?? "",
3637
+ buildVersion: resolved.identity.buildVersion ?? ""
3638
+ },
3639
+ runtime: resolved.runtime,
3640
+ enabled: false
3641
+ });
3642
+ delegating.replace(gateway.upstream);
3643
+ rawDiagnostic = () => gateway.diagnostic();
3644
+ ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
3645
+ } catch (error) {
3646
+ ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
3647
+ }
3648
+ })();
3649
+ stacks[region] = {
3650
+ region,
3651
+ store,
3652
+ catalog,
3653
+ shim,
3654
+ delegating,
3655
+ usageClient: new TraeUsageClient({ credential: () => store.resolve() }),
3656
+ discoverModels: async (signal) => {
3657
+ const [remote, wireModels] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
3658
+ const merged = mergeTraeModelSources(remote, wireModels);
3659
+ wire.callableKeys.clear();
3660
+ for (const model of merged) {
3661
+ wire.callableKeys.add(model.id.trim().toLowerCase());
3662
+ wire.callableKeys.add(model.name.trim().toLowerCase());
3663
+ }
3664
+ wire.resolved = merged.length > 0;
3665
+ wire.byId.clear();
3666
+ wire.byName.clear();
3667
+ for (const model of merged) if (model.wireConfigName !== void 0) {
3668
+ wire.byId.set(model.id, model.wireConfigName);
3669
+ wire.byName.set(model.name.trim().toLowerCase(), model.wireConfigName);
3670
+ }
3671
+ return merged;
3672
+ },
3673
+ invalidateAdapter: () => {},
3674
+ rawDiagnostic: () => rawDiagnostic()
3675
+ };
3676
+ }
3078
3677
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
3079
- store,
3080
- client: usageClient,
3081
- displayModels: () => displayModels(current()),
3082
- enabledModelIds: () => current().enabledModelIds ?? [],
3083
- discoverModels,
3084
- rawDiagnostic: () => rawDiagnostic()
3678
+ store: (region) => stacks[region].store,
3679
+ client: (region) => stacks[region].usageClient,
3680
+ displayModels: (region) => displayModels(current(), region),
3681
+ enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
3682
+ discoverModels: (region, signal) => stacks[region].discoverModels(signal),
3683
+ rawDiagnostic: (region) => stacks[region].rawDiagnostic()
3085
3684
  }));
3685
+ /** Push the current config into every region's store selection and catalog. */
3686
+ const applySelection = (value) => {
3687
+ for (const region of REGION_KEYS) {
3688
+ const stack = stacks[region];
3689
+ stack.store.setSource(value.authFile, value.edition ?? "auto", effectiveAccountFor(region, value));
3690
+ stack.catalog.set(configuredModels(value, region));
3691
+ stack.invalidateAdapter();
3692
+ }
3693
+ };
3086
3694
  const sectionHooks = {
3087
3695
  setSource(source) {
3088
3696
  current = source;
3089
3697
  },
3090
3698
  onChange() {
3091
- const next = current();
3092
- store.setSource(next.authFile, next.edition ?? "auto", next.accountId);
3093
- catalog.set(configuredModels(next));
3094
- invalidateAdapter();
3699
+ applySelection(current());
3095
3700
  }
3096
3701
  };
3702
+ applySelection(config);
3703
+ (async () => {
3704
+ const id = current().accountId;
3705
+ if (id === void 0) return;
3706
+ try {
3707
+ for (const region of REGION_KEYS) if ((await stacks[region].store.accounts()).some((account) => account.id === id)) {
3708
+ legacyAccountRegion = region;
3709
+ applySelection(current());
3710
+ return;
3711
+ }
3712
+ } catch {}
3713
+ })();
3097
3714
  const legacyInstallSettingsSection = dshSettings.installSettingsSection;
3098
3715
  if (typeof legacyInstallSettingsSection === "function") legacyInstallSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
3099
3716
  else ctx.inject(["settings"], (settingsCtx) => {
@@ -3102,32 +3719,34 @@ function apply(ctx, config) {
3102
3719
  let stopped = false;
3103
3720
  ctx.effect(() => () => {
3104
3721
  stopped = true;
3105
- shim.close();
3722
+ for (const region of REGION_KEYS) stacks[region].shim.close();
3106
3723
  });
3107
- shim.ready.then(async () => {
3724
+ Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
3108
3725
  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;
3726
+ let releaseAdapterCn;
3727
+ let releaseAdapterAi;
3124
3728
  let releaseDirectory;
3125
3729
  try {
3126
- releaseAdapter = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
3730
+ for (const region of REGION_KEYS) {
3731
+ const stack = stacks[region];
3732
+ const trae = createTraeAdapter({
3733
+ shim: stack.shim,
3734
+ catalog: stack.catalog,
3735
+ provider: TRAE_PROVIDERS[region],
3736
+ displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
3737
+ resolveAttachments: () => ctx.get("attachments")
3738
+ });
3739
+ stack.invalidateAdapter = () => {
3740
+ trae.invalidate();
3741
+ };
3742
+ if (region === "cn") releaseAdapterCn = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
3743
+ else releaseAdapterAi = ctx.llm.registerAdapter([TRAE_AI_PROVIDER], trae.adapter);
3744
+ }
3127
3745
  ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request, signal) => {
3128
- if (request.provider !== "trae") return [];
3746
+ const region = regionOfTraeProvider(request.provider ?? "");
3747
+ if (region === void 0) return [];
3129
3748
  const cancellation = signal ?? request.signal;
3130
- return applyImageSelection(await discoverModels(cancellation), imageSet(current())).map((model) => ({
3749
+ return applyImageSelection(await stacks[region].discoverModels(cancellation), imageSet(current(), region)).map((model) => ({
3131
3750
  id: model.id,
3132
3751
  name: traeModelDisplayName(model),
3133
3752
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
@@ -3135,31 +3754,46 @@ function apply(ctx, config) {
3135
3754
  inputModalities: traeInputModalities(model)
3136
3755
  }));
3137
3756
  });
3138
- releaseDirectory = ctx.llm.registerConfigurableProviders([{
3139
- provider: TRAE_PROVIDER,
3140
- displayName: "Trae",
3757
+ releaseDirectory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
3758
+ provider: TRAE_PROVIDERS[region],
3759
+ displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
3141
3760
  settingsNs: TRAE_SETTINGS_NS,
3142
3761
  settingsPath: [],
3143
3762
  declared: false
3144
- }]);
3763
+ })));
3145
3764
  } finally {
3146
- if (releaseAdapter === void 0 || releaseDirectory === void 0) {
3147
- releaseAdapter?.();
3765
+ if (releaseAdapterCn === void 0 || releaseAdapterAi === void 0 || releaseDirectory === void 0) {
3766
+ releaseAdapterCn?.();
3767
+ releaseAdapterAi?.();
3148
3768
  releaseDirectory?.();
3149
3769
  }
3150
3770
  }
3151
3771
  try {
3152
3772
  ctx.effect(() => () => {
3153
- releaseAdapter?.();
3773
+ releaseAdapterCn?.();
3774
+ releaseAdapterAi?.();
3154
3775
  releaseDirectory?.();
3155
3776
  });
3156
3777
  } catch {
3157
- releaseAdapter?.();
3778
+ releaseAdapterCn?.();
3779
+ releaseAdapterAi?.();
3158
3780
  releaseDirectory?.();
3159
3781
  }
3782
+ for (const region of REGION_KEYS) (async () => {
3783
+ const stack = stacks[region];
3784
+ try {
3785
+ const models = await stack.discoverModels();
3786
+ if (stopped) return;
3787
+ stack.catalog.set(derive(current(), models, region));
3788
+ } catch (error) {
3789
+ ctx.logger.warn(`dsh-connect-trae: live ${region} model directory unavailable; serving the configured catalog`, error);
3790
+ stack.catalog.set(configuredModels(current(), region));
3791
+ }
3792
+ stack.invalidateAdapter();
3793
+ })();
3160
3794
  }).catch((error) => {
3161
- ctx.logger.error("dsh-connect-trae: loopback shim failed; provider not registered", error);
3795
+ ctx.logger.error("dsh-connect-trae: loopback shim failed; providers not registered", error);
3162
3796
  });
3163
3797
  }
3164
3798
  //#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 };
3799
+ 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, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, withTraeRegion };