dsh-connect-trae 3.6.1 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/README.en.md +24 -7
- package/README.md +40 -10
- package/lib/client.js +92 -37
- package/lib/index.d.ts +312 -43
- package/lib/index.js +821 -277
- package/package.json +20 -19
package/lib/index.js
CHANGED
|
@@ -129,9 +129,16 @@ function traeInputModalities(model) {
|
|
|
129
129
|
* (wire resolution, callable-key filtering) must keep matching the
|
|
130
130
|
* undecorated name; only the model rows handed to DSH (adapter catalog and
|
|
131
131
|
* model discovery) use this decorated name.
|
|
132
|
+
*
|
|
133
|
+
* `withRate` is false for the ENTERPRISE panel. The multiplier is a
|
|
134
|
+
* consumer-pool figure — personal credits per token, read from the consumer
|
|
135
|
+
* directory — and the tenant pool does not bill against it, so printing it in
|
|
136
|
+
* the model picker would price a model the tenant is not charged for. The rate
|
|
137
|
+
* is part of the NAME that reaches DSH, which is why this is a parameter here
|
|
138
|
+
* and not a rendering decision downstream.
|
|
132
139
|
*/
|
|
133
|
-
function traeModelDisplayName(model) {
|
|
134
|
-
return model.creditMultiplier === void 0 ? model.name : `${model.name} · x${model.creditMultiplier.toFixed(2)}`;
|
|
140
|
+
function traeModelDisplayName(model, withRate = true) {
|
|
141
|
+
return !withRate || model.creditMultiplier === void 0 ? model.name : `${model.name} · x${model.creditMultiplier.toFixed(2)}`;
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* Whether one model should be offered image input.
|
|
@@ -344,9 +351,10 @@ var TraeCatalog = class {
|
|
|
344
351
|
*
|
|
345
352
|
* The mismatch is reachable in BOTH directions, because DSH itself spans both
|
|
346
353
|
* generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
|
|
347
|
-
* later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2
|
|
348
|
-
*
|
|
349
|
-
* the
|
|
354
|
+
* later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`, and 0.2.1-rc.1 —
|
|
355
|
+
* the current desktop host — declares **`^1.1.0`**, which also stopped reading
|
|
356
|
+
* the top-level fields entirely), while this plugin accepts the whole range
|
|
357
|
+
* (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer, the other one is wrong:
|
|
350
358
|
*
|
|
351
359
|
* | host | resolved api | outcome |
|
|
352
360
|
* |---|---|---|
|
|
@@ -498,24 +506,61 @@ function readManifest(directory) {
|
|
|
498
506
|
/** Provider route this bundle owns for the domestic (CN) gateway. */
|
|
499
507
|
const TRAE_PROVIDER = "trae";
|
|
500
508
|
/**
|
|
509
|
+
* Provider route this bundle owns for the CN **enterprise (tenant)** gateway.
|
|
510
|
+
*
|
|
511
|
+
* A separate provider id, not a mode switch, because the two pools bill
|
|
512
|
+
* differently: a tenant account (`tob_online`) is served by BOTH gateways with
|
|
513
|
+
* the SAME credential, and only the enterprise one draws on the enterprise
|
|
514
|
+
* pool. Exposing one id would force a per-call guess about who pays.
|
|
515
|
+
*
|
|
516
|
+
* It never falls back to the consumer pool: picking this provider is the user
|
|
517
|
+
* saying "bill the tenant", so a refusal is reported as an error instead of
|
|
518
|
+
* being silently re-run against the personal quota.
|
|
519
|
+
*/
|
|
520
|
+
const TRAE_ENTERPRISE_PROVIDER = "trae-enterprise";
|
|
521
|
+
/**
|
|
501
522
|
* Provider route this bundle owns for the international (trae.ai) gateway.
|
|
502
523
|
* Named `-global` to match the plugin family's convention (`workbuddy-global`);
|
|
503
524
|
* the internal region bucket stays `ai`, which is the gateway/protocol name.
|
|
504
525
|
*/
|
|
505
526
|
const TRAE_AI_PROVIDER = "trae-global";
|
|
506
|
-
/** The provider
|
|
527
|
+
/** The provider ids one region publishes, in model-picker order. */
|
|
528
|
+
const TRAE_PROVIDER_IDS = {
|
|
529
|
+
cn: [TRAE_PROVIDER, TRAE_ENTERPRISE_PROVIDER],
|
|
530
|
+
ai: [TRAE_AI_PROVIDER]
|
|
531
|
+
};
|
|
532
|
+
/** Each region's default (first) provider id. */
|
|
507
533
|
const TRAE_PROVIDERS = {
|
|
508
534
|
cn: TRAE_PROVIDER,
|
|
509
535
|
ai: TRAE_AI_PROVIDER
|
|
510
536
|
};
|
|
537
|
+
const TRAE_PROVIDER_ROUTES = {
|
|
538
|
+
[TRAE_PROVIDER]: "solo",
|
|
539
|
+
[TRAE_ENTERPRISE_PROVIDER]: "enterprise",
|
|
540
|
+
[TRAE_AI_PROVIDER]: "solo"
|
|
541
|
+
};
|
|
542
|
+
/** Chat pool a provider id bills against; `undefined` for an unknown id. */
|
|
543
|
+
function routeOfTraeProvider(provider) {
|
|
544
|
+
return TRAE_PROVIDER_ROUTES[provider];
|
|
545
|
+
}
|
|
511
546
|
/** Human-readable provider names, shown in the DSH model picker. */
|
|
512
547
|
const TRAE_PROVIDER_DISPLAY_NAMES = {
|
|
513
548
|
cn: "Trae",
|
|
514
549
|
ai: "Trae Global"
|
|
515
550
|
};
|
|
551
|
+
/** Display name per provider id (the region table above only names the default). */
|
|
552
|
+
const TRAE_PROVIDER_ID_DISPLAY_NAMES = {
|
|
553
|
+
[TRAE_PROVIDER]: "Trae",
|
|
554
|
+
[TRAE_ENTERPRISE_PROVIDER]: "Trae 企业版",
|
|
555
|
+
[TRAE_AI_PROVIDER]: "Trae Global"
|
|
556
|
+
};
|
|
557
|
+
/** Display name of a provider id. */
|
|
558
|
+
function displayNameOfTraeProvider(provider) {
|
|
559
|
+
return TRAE_PROVIDER_ID_DISPLAY_NAMES[provider] ?? provider;
|
|
560
|
+
}
|
|
516
561
|
/** Region a provider route id belongs to. */
|
|
517
562
|
function regionOfTraeProvider(provider) {
|
|
518
|
-
for (const [region,
|
|
563
|
+
for (const [region, ids] of Object.entries(TRAE_PROVIDER_IDS)) if (ids.includes(provider)) return region;
|
|
519
564
|
}
|
|
520
565
|
const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
521
566
|
const INERT_AUTH = {
|
|
@@ -548,10 +593,10 @@ const REQUEST_IMAGE_BUDGETS = {
|
|
|
548
593
|
* failing the entire provider route (`INVALID_MODEL_CONTEXT`).
|
|
549
594
|
*/
|
|
550
595
|
const FALLBACK_CONTEXT_WINDOW = 2e5;
|
|
551
|
-
function toPiModel(info, baseUrl, providerId) {
|
|
596
|
+
function toPiModel(info, baseUrl, providerId, withRate = true) {
|
|
552
597
|
return {
|
|
553
598
|
id: info.id,
|
|
554
|
-
name: traeModelDisplayName(info),
|
|
599
|
+
name: traeModelDisplayName(info, withRate),
|
|
555
600
|
api: "openai-completions",
|
|
556
601
|
provider: providerId,
|
|
557
602
|
baseUrl,
|
|
@@ -689,13 +734,17 @@ function adaptLegacyPiAiContext(context) {
|
|
|
689
734
|
* transcript a MODERN (0.86+) api consumes — the reverse mismatch, and just as
|
|
690
735
|
* silent.
|
|
691
736
|
*
|
|
692
|
-
* WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1
|
|
737
|
+
* WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1`,
|
|
738
|
+
* while the 0.2.1-rc.1 desktop host pins `^1.1.0` and still emits the legacy
|
|
739
|
+
* envelope from its own `piContext()`)
|
|
693
740
|
* while this plugin may resolve a newer copy than its host. A LEGACY host never
|
|
694
741
|
* normalizes, so it hands the api the legacy shape; a modern api reads the
|
|
695
742
|
* prompt from a leading system message and the tools from its `toolsAdded`, and
|
|
696
743
|
* never looks at the top-level fields — so both are dropped without a sound.
|
|
697
744
|
* Verified against the published tarballs: the request that reaches the wire
|
|
698
|
-
* carries no system turn and `tools: []`.
|
|
745
|
+
* carries no system turn and `tools: []`. This is exactly the shape that
|
|
746
|
+
* `dsh-llm-pi-ai@0.2.1-rc.1` + `pi-ai@1.1.0` would hit unaided, which is why
|
|
747
|
+
* the wire-level matrix in `tests/pi-ai-context.spec.ts` pins both directions.
|
|
699
748
|
*
|
|
700
749
|
* Identity for anything already in transcript shape, and for a context with
|
|
701
750
|
* neither a prompt nor tools (pi-ai's own `createInitialSystemMessage` returns
|
|
@@ -730,20 +779,47 @@ function adaptModernPiAiContext(context) {
|
|
|
730
779
|
* no context and are passed through untouched, and each adapter function is an
|
|
731
780
|
* identity unless the context is actually the other generation's shape. So a
|
|
732
781
|
* healthy install does one `Array.some` per request.
|
|
782
|
+
*
|
|
783
|
+
* THE CAST IS THE CONTRACT (pi-ai 1.1.0). From 1.1.0 `ProviderStreams` takes
|
|
784
|
+
* `TranscriptContext`, a BRANDED type whose `unique symbol` member only
|
|
785
|
+
* `normalizeContext()` can produce — so a `Context` is no longer assignable and
|
|
786
|
+
* `tsc` reports `TS2345` at both call sites below. The brand cannot be acquired
|
|
787
|
+
* honestly here, for two reasons that are both deliberate:
|
|
788
|
+
*
|
|
789
|
+
* 1. This wrapper sits BELOW the normalizer. `Models.stream()`/`streamSimple()`
|
|
790
|
+
* call `normalizeContext()` and hand a real transcript to the provider;
|
|
791
|
+
* `createProvider()` passes it straight through to `api.stream`. So on the
|
|
792
|
+
* modern path `adapt()` is an identity and the value already IS a branded
|
|
793
|
+
* transcript at runtime — the type just cannot see it across that boundary.
|
|
794
|
+
* 2. The legacy path REQUIRES an unbranded shape. pi-ai ≤0.85 has no
|
|
795
|
+
* `normalizeContext` and no transcript type at all, so its api expects
|
|
796
|
+
* exactly the `{ systemPrompt, tools, messages }` object `adapt()` returns.
|
|
797
|
+
*
|
|
798
|
+
* So the value handed to `api` is "a context of the generation that api
|
|
799
|
+
* speaks", which no single edition's declared type names. Casting at this one
|
|
800
|
+
* boundary is what makes both generations expressible; deriving the type from
|
|
801
|
+
* the resolved copy instead would make the wrapper uncompilable against the
|
|
802
|
+
* other generation, which is the whole point of having it.
|
|
733
803
|
*/
|
|
734
804
|
function withContextAdaptation(api, generation) {
|
|
735
805
|
const adapt = generation === "modern" ? adaptModernPiAiContext : adaptLegacyPiAiContext;
|
|
806
|
+
const forApi = (context) => adapt(context);
|
|
736
807
|
return {
|
|
737
808
|
...api,
|
|
738
|
-
stream: (model, context, options) => api.stream(model,
|
|
739
|
-
streamSimple: (model, context, options) => api.streamSimple(model,
|
|
809
|
+
stream: (model, context, options) => api.stream(model, forApi(context), options),
|
|
810
|
+
streamSimple: (model, context, options) => api.streamSimple(model, forApi(context), options)
|
|
740
811
|
};
|
|
741
812
|
}
|
|
742
813
|
function createTraeAdapter(options) {
|
|
743
814
|
const providerId = options.provider ?? "trae";
|
|
744
815
|
const providerName = options.displayName ?? "Trae";
|
|
745
816
|
const generation = options.piAiGeneration ?? piAiRuntimeInfo().generation;
|
|
746
|
-
|
|
817
|
+
/**
|
|
818
|
+
* The enterprise provider shows no rate: the multiplier belongs to the
|
|
819
|
+
* consumer pool, and this provider bills the tenant pool.
|
|
820
|
+
*/
|
|
821
|
+
const showRate = options.provider !== TRAE_ENTERPRISE_PROVIDER;
|
|
822
|
+
const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId, showRate));
|
|
747
823
|
const provider = {
|
|
748
824
|
...createProvider({
|
|
749
825
|
id: providerId,
|
|
@@ -779,7 +855,7 @@ function createTraeAdapter(options) {
|
|
|
779
855
|
adapter: new PiAiAdapter({
|
|
780
856
|
profiles: () => profiles,
|
|
781
857
|
auth: INERT_AUTH,
|
|
782
|
-
resolveApiKey: async () => options.shim.token(),
|
|
858
|
+
resolveApiKey: async () => options.secret ?? options.shim.token(),
|
|
783
859
|
...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
|
|
784
860
|
}),
|
|
785
861
|
invalidate() {
|
|
@@ -788,7 +864,113 @@ function createTraeAdapter(options) {
|
|
|
788
864
|
};
|
|
789
865
|
}
|
|
790
866
|
//#endregion
|
|
867
|
+
//#region src/region.ts
|
|
868
|
+
/** Every panel, in card tab order. */
|
|
869
|
+
const TRAE_PANELS = [
|
|
870
|
+
"cn",
|
|
871
|
+
"enterprise",
|
|
872
|
+
"ai"
|
|
873
|
+
];
|
|
874
|
+
/** The region whose credential/account pool a panel draws on. */
|
|
875
|
+
function regionOfPanel(panel) {
|
|
876
|
+
return panel === "ai" ? "ai" : "cn";
|
|
877
|
+
}
|
|
878
|
+
/** The provider id a panel publishes. */
|
|
879
|
+
function providerOfPanel(panel) {
|
|
880
|
+
if (panel === "enterprise") return "trae-enterprise";
|
|
881
|
+
return panel === "ai" ? "trae-global" : "trae";
|
|
882
|
+
}
|
|
883
|
+
/** The panel that owns a provider id, or undefined for an unknown id. */
|
|
884
|
+
function panelOfProvider(provider) {
|
|
885
|
+
return TRAE_PANELS.find((panel) => providerOfPanel(panel) === provider);
|
|
886
|
+
}
|
|
887
|
+
/** Narrow a stored settings key to a panel. */
|
|
888
|
+
function panelOfKey(value) {
|
|
889
|
+
return TRAE_PANELS.includes(value) ? value : void 0;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
|
|
893
|
+
* The AI gateway is shared by both international installs (Trae desktop and
|
|
894
|
+
* TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
|
|
895
|
+
* answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
|
|
896
|
+
* single stable entry point.
|
|
897
|
+
*/
|
|
898
|
+
const REGION_GATEWAYS = {
|
|
899
|
+
cn: {
|
|
900
|
+
chat: "https://trae-api-cn.mchost.guru",
|
|
901
|
+
remote: "https://solo.trae.cn/api/remote/v1",
|
|
902
|
+
pay: "https://api.trae.cn"
|
|
903
|
+
},
|
|
904
|
+
ai: {
|
|
905
|
+
chat: "https://coresg-normal.trae.ai",
|
|
906
|
+
remote: "https://coresg-normal.trae.ai/api/remote/v1",
|
|
907
|
+
pay: "https://growsg-normal.trae.ai"
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
/**
|
|
911
|
+
* Enterprise (tenant) chat gateway for a CN credential that belongs to a
|
|
912
|
+
* `tob_online` tenant.
|
|
913
|
+
*
|
|
914
|
+
* This is a SECOND chat gateway on the same credential, not a second region:
|
|
915
|
+
* the roster, the credential and the protocol are the same as `cn`, but a
|
|
916
|
+
* tenant account is billed against the enterprise pool when the request goes
|
|
917
|
+
* here. Measured 2026-10-17 on a live `Trae CN` tenant account: the same model
|
|
918
|
+
* served from this host does not move the personal `consumed_amount`, while
|
|
919
|
+
* the same call to `trae-api-cn.mchost.guru` charges it.
|
|
920
|
+
*
|
|
921
|
+
* Two wire differences, both load-bearing:
|
|
922
|
+
* - the request must carry a per-call `session_id` (the consumer gateway
|
|
923
|
+
* rejects it with `4001`), and
|
|
924
|
+
* - a refused call answers **HTTP 200** whose FIRST SSE frame is
|
|
925
|
+
* `event:error` with `code: 4001` — not an HTTP error status, so the
|
|
926
|
+
* refusal is only visible if the response body is inspected.
|
|
927
|
+
*/
|
|
928
|
+
const TRAE_ENTERPRISE_HOST = "https://console.enterprise.trae.cn";
|
|
929
|
+
/** Region for an edition label: the international installs belong to `ai`. */
|
|
930
|
+
function regionOfEdition(edition) {
|
|
931
|
+
return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Region from the credential's `userRegion` claim. The desktop storage spells
|
|
935
|
+
* it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
|
|
936
|
+
* spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
|
|
937
|
+
*/
|
|
938
|
+
function regionOfUserRegion(value) {
|
|
939
|
+
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
940
|
+
if (typeof raw !== "string") return void 0;
|
|
941
|
+
const lowered = raw.trim().toLowerCase();
|
|
942
|
+
if (lowered === "cn") return "cn";
|
|
943
|
+
if (lowered === "sg" || lowered === "ai") return "ai";
|
|
944
|
+
}
|
|
945
|
+
/** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
|
|
946
|
+
function regionOfHost(host) {
|
|
947
|
+
if (host === void 0) return void 0;
|
|
948
|
+
const trimmed = host.trim();
|
|
949
|
+
if (trimmed === "") return void 0;
|
|
950
|
+
let hostname;
|
|
951
|
+
try {
|
|
952
|
+
hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
|
|
953
|
+
} catch {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
|
|
957
|
+
if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Region of a credential: the `userRegion` claim wins, the host suffix is the
|
|
961
|
+
* fallback, and the edition label is the last resort. Every level is derived
|
|
962
|
+
* from data the credential itself carries, so no user configuration is needed.
|
|
963
|
+
*/
|
|
964
|
+
function regionOfCredential(credential) {
|
|
965
|
+
return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
|
|
966
|
+
}
|
|
967
|
+
//#endregion
|
|
791
968
|
//#region src/status-paths.ts
|
|
969
|
+
/**
|
|
970
|
+
* Node-free constants and types shared by the Host and browser halves.
|
|
971
|
+
* Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
|
|
972
|
+
* stays consistent with that project's external presentation.
|
|
973
|
+
*/
|
|
792
974
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
793
975
|
const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
|
|
794
976
|
/** Plugin-owned live model refresh endpoint. */
|
|
@@ -855,8 +1037,8 @@ const TRAE_REGIONS = ["cn", "ai"];
|
|
|
855
1037
|
* Address one region's status route. The two regions are separate provider
|
|
856
1038
|
* stacks; every card request carries the region whose tab the user is on.
|
|
857
1039
|
*/
|
|
858
|
-
function withTraeRegion(path,
|
|
859
|
-
return `${path}?${TRAE_REGION_PARAM}=${
|
|
1040
|
+
function withTraeRegion(path, panel) {
|
|
1041
|
+
return `${path}?${TRAE_REGION_PARAM}=${panel}`;
|
|
860
1042
|
}
|
|
861
1043
|
/**
|
|
862
1044
|
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
@@ -867,7 +1049,7 @@ function regionOfTraeStatusUrl(url) {
|
|
|
867
1049
|
const at = url.indexOf("?");
|
|
868
1050
|
const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
|
|
869
1051
|
if (value === null || value === "") return "cn";
|
|
870
|
-
return
|
|
1052
|
+
return TRAE_PANELS.includes(value) ? value : void 0;
|
|
871
1053
|
}
|
|
872
1054
|
/**
|
|
873
1055
|
* Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
|
|
@@ -944,11 +1126,11 @@ function followAppLoginEnabled(configured) {
|
|
|
944
1126
|
* region's picks. Tolerates any stored shape (absent, non-object) by starting
|
|
945
1127
|
* from an empty document.
|
|
946
1128
|
*/
|
|
947
|
-
function nextRegionSlots(regions,
|
|
1129
|
+
function nextRegionSlots(regions, panel, slot) {
|
|
948
1130
|
const unwrapped = unwrapVolatileDeep(regions);
|
|
949
1131
|
return {
|
|
950
1132
|
...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
|
|
951
|
-
[
|
|
1133
|
+
[panel]: slot
|
|
952
1134
|
};
|
|
953
1135
|
}
|
|
954
1136
|
/**
|
|
@@ -973,8 +1155,8 @@ function regionsMapOf(value) {
|
|
|
973
1155
|
return record;
|
|
974
1156
|
}
|
|
975
1157
|
/** One region's stored slot as a plain object; any other shape reads as empty. */
|
|
976
|
-
function regionSlotOf(value,
|
|
977
|
-
const slot = unwrapVolatile(regionsMapOf(value)[
|
|
1158
|
+
function regionSlotOf(value, panel) {
|
|
1159
|
+
const slot = unwrapVolatile(regionsMapOf(value)[panel]);
|
|
978
1160
|
return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
|
|
979
1161
|
}
|
|
980
1162
|
/**
|
|
@@ -987,8 +1169,10 @@ function regionSlotOf(value, region) {
|
|
|
987
1169
|
* `value` may be the whole settings section or the `regions` map (see
|
|
988
1170
|
* {@link regionsMapOf}).
|
|
989
1171
|
*/
|
|
990
|
-
function regionEnabledOf(value,
|
|
991
|
-
|
|
1172
|
+
function regionEnabledOf(value, panel) {
|
|
1173
|
+
const stored = regionSlotOf(value, panel)["enabled"];
|
|
1174
|
+
if (typeof stored === "boolean") return stored;
|
|
1175
|
+
return panel !== "enterprise";
|
|
992
1176
|
}
|
|
993
1177
|
/**
|
|
994
1178
|
* Build the next `regions` settings value for a provider on/off toggle. ONLY
|
|
@@ -1006,9 +1190,9 @@ function regionEnabledOf(value, region) {
|
|
|
1006
1190
|
* value is always the `regions` map, i.e. exactly what `settingsScope.set(
|
|
1007
1191
|
* 'regions', ...)` needs.
|
|
1008
1192
|
*/
|
|
1009
|
-
function nextRegionEnabled(value,
|
|
1010
|
-
return nextRegionSlots(regionsMapOf(value),
|
|
1011
|
-
...regionSlotOf(value,
|
|
1193
|
+
function nextRegionEnabled(value, panel, enabled) {
|
|
1194
|
+
return nextRegionSlots(regionsMapOf(value), panel, {
|
|
1195
|
+
...regionSlotOf(value, panel),
|
|
1012
1196
|
enabled
|
|
1013
1197
|
});
|
|
1014
1198
|
}
|
|
@@ -1025,8 +1209,8 @@ function nextRegionEnabled(value, region, enabled) {
|
|
|
1025
1209
|
* `value` may be the whole settings section or the `regions` map (see
|
|
1026
1210
|
* {@link regionsMapOf}).
|
|
1027
1211
|
*/
|
|
1028
|
-
function regionCreditPositionOf(value,
|
|
1029
|
-
return regionSlotOf(value,
|
|
1212
|
+
function regionCreditPositionOf(value, panel) {
|
|
1213
|
+
return regionSlotOf(value, panel)["creditPosition"] === "stats" ? "stats" : "composer";
|
|
1030
1214
|
}
|
|
1031
1215
|
/**
|
|
1032
1216
|
* Build the next `regions` settings value for one region's credit seat.
|
|
@@ -1039,9 +1223,9 @@ function regionCreditPositionOf(value, region) {
|
|
|
1039
1223
|
* value is always the `regions` map, i.e. exactly what `settingsScope.set(
|
|
1040
1224
|
* 'regions', ...)` needs.
|
|
1041
1225
|
*/
|
|
1042
|
-
function nextRegionCreditPosition(value,
|
|
1043
|
-
return nextRegionSlots(regionsMapOf(value),
|
|
1044
|
-
...regionSlotOf(value,
|
|
1226
|
+
function nextRegionCreditPosition(value, panel, position) {
|
|
1227
|
+
return nextRegionSlots(regionsMapOf(value), panel, {
|
|
1228
|
+
...regionSlotOf(value, panel),
|
|
1045
1229
|
creditPosition: position
|
|
1046
1230
|
});
|
|
1047
1231
|
}
|
|
@@ -1537,65 +1721,6 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
|
|
|
1537
1721
|
return result;
|
|
1538
1722
|
}
|
|
1539
1723
|
//#endregion
|
|
1540
|
-
//#region src/region.ts
|
|
1541
|
-
/**
|
|
1542
|
-
* Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
|
|
1543
|
-
* The AI gateway is shared by both international installs (Trae desktop and
|
|
1544
|
-
* TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
|
|
1545
|
-
* answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
|
|
1546
|
-
* single stable entry point.
|
|
1547
|
-
*/
|
|
1548
|
-
const REGION_GATEWAYS = {
|
|
1549
|
-
cn: {
|
|
1550
|
-
chat: "https://trae-api-cn.mchost.guru",
|
|
1551
|
-
remote: "https://solo.trae.cn/api/remote/v1",
|
|
1552
|
-
pay: "https://api.trae.cn"
|
|
1553
|
-
},
|
|
1554
|
-
ai: {
|
|
1555
|
-
chat: "https://coresg-normal.trae.ai",
|
|
1556
|
-
remote: "https://coresg-normal.trae.ai/api/remote/v1",
|
|
1557
|
-
pay: "https://growsg-normal.trae.ai"
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
/** Region for an edition label: the international installs belong to `ai`. */
|
|
1561
|
-
function regionOfEdition(edition) {
|
|
1562
|
-
return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Region from the credential's `userRegion` claim. The desktop storage spells
|
|
1566
|
-
* it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
|
|
1567
|
-
* spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
|
|
1568
|
-
*/
|
|
1569
|
-
function regionOfUserRegion(value) {
|
|
1570
|
-
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
1571
|
-
if (typeof raw !== "string") return void 0;
|
|
1572
|
-
const lowered = raw.trim().toLowerCase();
|
|
1573
|
-
if (lowered === "cn") return "cn";
|
|
1574
|
-
if (lowered === "sg" || lowered === "ai") return "ai";
|
|
1575
|
-
}
|
|
1576
|
-
/** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
|
|
1577
|
-
function regionOfHost(host) {
|
|
1578
|
-
if (host === void 0) return void 0;
|
|
1579
|
-
const trimmed = host.trim();
|
|
1580
|
-
if (trimmed === "") return void 0;
|
|
1581
|
-
let hostname;
|
|
1582
|
-
try {
|
|
1583
|
-
hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
|
|
1584
|
-
} catch {
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
|
|
1588
|
-
if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Region of a credential: the `userRegion` claim wins, the host suffix is the
|
|
1592
|
-
* fallback, and the edition label is the last resort. Every level is derived
|
|
1593
|
-
* from data the credential itself carries, so no user configuration is needed.
|
|
1594
|
-
*/
|
|
1595
|
-
function regionOfCredential(credential) {
|
|
1596
|
-
return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
|
|
1597
|
-
}
|
|
1598
|
-
//#endregion
|
|
1599
1724
|
//#region src/auth.ts
|
|
1600
1725
|
/** Host used for CLI tokens, which carry no host claim of their own. */
|
|
1601
1726
|
const CLI_DEFAULT_HOST = "https://api.trae.cn";
|
|
@@ -1661,12 +1786,15 @@ function normalizeTraeCredential(raw, edition, source) {
|
|
|
1661
1786
|
const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
|
|
1662
1787
|
const refreshToken = optionalString(value["refreshToken"]);
|
|
1663
1788
|
const userRegion = userRegionOf(value["userRegion"]);
|
|
1664
|
-
const
|
|
1789
|
+
const account = typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0;
|
|
1790
|
+
const accountName = optionalString(account?.["username"]);
|
|
1791
|
+
const tenantId = optionalString(account?.["tenant_id"]) ?? (typeof account?.["tenant_id"] === "number" ? String(account["tenant_id"]) : void 0);
|
|
1665
1792
|
return {
|
|
1666
1793
|
accessToken,
|
|
1667
1794
|
...refreshToken === void 0 ? {} : { refreshToken },
|
|
1668
1795
|
userId: optionalString(value["userId"]) ?? "",
|
|
1669
1796
|
...accountName === void 0 ? {} : { accountName },
|
|
1797
|
+
...tenantId === void 0 ? {} : { tenantId },
|
|
1670
1798
|
host: optionalString(value["host"]) ?? "",
|
|
1671
1799
|
...userRegion === void 0 ? {} : { userRegion },
|
|
1672
1800
|
expiresAtMs,
|
|
@@ -1675,6 +1803,18 @@ function normalizeTraeCredential(raw, edition, source) {
|
|
|
1675
1803
|
source
|
|
1676
1804
|
};
|
|
1677
1805
|
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
|
|
1808
|
+
* pool.
|
|
1809
|
+
*
|
|
1810
|
+
* Evidence (2026-10-17, live): the tenant's own account carries
|
|
1811
|
+
* `account.tenant_id` / `tenant_name` and `scope: 'saas'`; the personal SOLO
|
|
1812
|
+
* account carries neither and reports `scope: 'marscode'`. Only the tenant one
|
|
1813
|
+
* streams on the enterprise gateway — the other answers `4019` risk control.
|
|
1814
|
+
*/
|
|
1815
|
+
function isEnterpriseAccount(credential) {
|
|
1816
|
+
return credential?.tenantId !== void 0 && credential.tenantId.trim() !== "";
|
|
1817
|
+
}
|
|
1678
1818
|
function traeAccountId(credential) {
|
|
1679
1819
|
const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
|
|
1680
1820
|
return createHash("sha256").update(stable).digest("hex").slice(0, 24);
|
|
@@ -1691,7 +1831,10 @@ function credentialOfStored(stored) {
|
|
|
1691
1831
|
userId: record["userId"],
|
|
1692
1832
|
host: record["host"],
|
|
1693
1833
|
userRegion: record["userRegion"],
|
|
1694
|
-
account: record["accountName"] === void 0 ? void 0 : {
|
|
1834
|
+
account: record["accountName"] === void 0 && record["tenantId"] === void 0 ? void 0 : {
|
|
1835
|
+
...record["accountName"] === void 0 ? {} : { username: record["accountName"] },
|
|
1836
|
+
...record["tenantId"] === void 0 ? {} : { tenant_id: record["tenantId"] }
|
|
1837
|
+
},
|
|
1695
1838
|
expiredAt: record["expiresAtMs"],
|
|
1696
1839
|
refreshExpiredAt: record["refreshExpiredAtMs"]
|
|
1697
1840
|
}, edition, "dsh");
|
|
@@ -1746,6 +1889,8 @@ var TraeCredentialStore = class {
|
|
|
1746
1889
|
edition;
|
|
1747
1890
|
accountId;
|
|
1748
1891
|
region;
|
|
1892
|
+
requireTenant;
|
|
1893
|
+
hideTenantAccounts;
|
|
1749
1894
|
ownPathExplicit;
|
|
1750
1895
|
legacyOwnPath;
|
|
1751
1896
|
legacyOwnPathExplicit;
|
|
@@ -1757,6 +1902,8 @@ var TraeCredentialStore = class {
|
|
|
1757
1902
|
this.edition = options.edition ?? "auto";
|
|
1758
1903
|
this.accountId = options.accountId;
|
|
1759
1904
|
this.region = options.region;
|
|
1905
|
+
this.requireTenant = options.requireTenant === true;
|
|
1906
|
+
this.hideTenantAccounts = options.hideTenantAccounts === true;
|
|
1760
1907
|
this.ownPathExplicit = options.ownPath;
|
|
1761
1908
|
this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
|
|
1762
1909
|
this.legacyOwnPathExplicit = options.legacyOwnPath;
|
|
@@ -1840,7 +1987,9 @@ var TraeCredentialStore = class {
|
|
|
1840
1987
|
* the plugin performed is never discarded by this preference.
|
|
1841
1988
|
*/
|
|
1842
1989
|
preferred(credentials) {
|
|
1843
|
-
|
|
1990
|
+
const eligible = this.requireTenant ? credentials.filter((credential) => isEnterpriseAccount(credential)) : credentials;
|
|
1991
|
+
if (eligible.length === 0) return void 0;
|
|
1992
|
+
return eligible.find((credential) => credential.source === "desktop") ?? eligible[0];
|
|
1844
1993
|
}
|
|
1845
1994
|
/**
|
|
1846
1995
|
* The account the Trae app is signed in to right now, when that differs from
|
|
@@ -1864,6 +2013,7 @@ var TraeCredentialStore = class {
|
|
|
1864
2013
|
id: liveId,
|
|
1865
2014
|
accountName: live.accountName ?? (live.userId || `${live.edition} account`),
|
|
1866
2015
|
edition: live.edition,
|
|
2016
|
+
enterprise: isEnterpriseAccount(live),
|
|
1867
2017
|
region: regionOfCredential(live),
|
|
1868
2018
|
source: live.source,
|
|
1869
2019
|
tokenExpiresAtMs: live.expiresAtMs,
|
|
@@ -1871,25 +2021,94 @@ var TraeCredentialStore = class {
|
|
|
1871
2021
|
selected: false
|
|
1872
2022
|
};
|
|
1873
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* The accounts this store is willing to OFFER.
|
|
2026
|
+
*
|
|
2027
|
+
* Scoped to who this store may bill:
|
|
2028
|
+
* - a consumer store withholds tenant accounts (the enterprise pool is a
|
|
2029
|
+
* separate tab and a separate billing decision), and
|
|
2030
|
+
* - a tenant store withholds personal accounts (they cannot bill that
|
|
2031
|
+
* pool; the tenant gateway answers their chat with 4019).
|
|
2032
|
+
*
|
|
2033
|
+
* Either filter backs off when it would hide EVERYTHING, so a machine whose
|
|
2034
|
+
* only login is on the other side keeps working instead of reading as
|
|
2035
|
+
* "signed out".
|
|
2036
|
+
*/
|
|
2037
|
+
offered(discovered) {
|
|
2038
|
+
const eligible = this.requireTenant ? discovered.filter((credential) => isEnterpriseAccount(credential)) : this.hideTenantAccounts ? discovered.filter((credential) => !isEnterpriseAccount(credential)) : discovered;
|
|
2039
|
+
return eligible.length > 0 ? eligible : discovered;
|
|
2040
|
+
}
|
|
1874
2041
|
async accounts() {
|
|
1875
|
-
const
|
|
1876
|
-
const
|
|
1877
|
-
const
|
|
2042
|
+
const discovered = await this.readAll();
|
|
2043
|
+
const offered = this.offered(discovered);
|
|
2044
|
+
const inUse = this.inUse(discovered, offered);
|
|
2045
|
+
/**
|
|
2046
|
+
* Never hide the account we are actually billing.
|
|
2047
|
+
*
|
|
2048
|
+
* An explicit binding outranks the display filter (`inUse` honours it even
|
|
2049
|
+
* when the offered list withholds it), and a list whose active row is
|
|
2050
|
+
* invisible is worse than no list: the card's "save the selected account"
|
|
2051
|
+
* press reads `selected`, so a hidden binding would be rewritten to whichever
|
|
2052
|
+
* account merely happened to be DISPLAYED — silently moving the user's
|
|
2053
|
+
* billing to an account they never chose. Re-appending it costs one extra
|
|
2054
|
+
* row and makes the card tell the truth.
|
|
2055
|
+
*/
|
|
2056
|
+
const credentials = inUse !== void 0 && !offered.includes(inUse) ? [...offered, inUse] : offered;
|
|
2057
|
+
const inUseId = inUse === void 0 ? void 0 : traeAccountId(inUse);
|
|
1878
2058
|
return credentials.map((credential) => ({
|
|
1879
2059
|
id: traeAccountId(credential),
|
|
1880
2060
|
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
|
|
1881
2061
|
edition: credential.edition,
|
|
2062
|
+
enterprise: isEnterpriseAccount(credential),
|
|
1882
2063
|
region: regionOfCredential(credential),
|
|
1883
2064
|
source: credential.source,
|
|
1884
2065
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
1885
2066
|
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
1886
|
-
selected:
|
|
2067
|
+
selected: inUseId !== void 0 && traeAccountId(credential) === inUseId
|
|
1887
2068
|
}));
|
|
1888
2069
|
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The credential this store bills, given every credential it can see.
|
|
2072
|
+
*
|
|
2073
|
+
* This is the ONE resolution rule, and it exists so that `current()` and
|
|
2074
|
+
* `accounts()` cannot disagree: `current()` applies it to `readAll()`, and
|
|
2075
|
+
* `accounts()` applies the SAME verdict to decide which row reads as
|
|
2076
|
+
* `selected`. Deriving that flag independently is exactly how the two drifted
|
|
2077
|
+
* apart — and a card that marks a different account than the one the chat
|
|
2078
|
+
* bills is worse than no card at all, because a press that "saves the
|
|
2079
|
+
* selected account" then rewrites the binding to the account that was only
|
|
2080
|
+
* ever DISPLAYED.
|
|
2081
|
+
*
|
|
2082
|
+
* Order of business:
|
|
2083
|
+
* 1. No binding — the detected default (`preferred`) is in use, chosen from
|
|
2084
|
+
* the OFFERED list rather than from every login on the machine. A
|
|
2085
|
+
* consumer store that withholds tenant accounts must not default its
|
|
2086
|
+
* billing to the tenant pool merely because that login sorts first;
|
|
2087
|
+
* withholding is a billing decision, not a display detail.
|
|
2088
|
+
* 2. A binding — honour it verbatim, even when the OFFERED list hides that
|
|
2089
|
+
* account. Resolution is never scoped by `hideTenantAccounts`: the user's
|
|
2090
|
+
* explicit choice outranks a display filter.
|
|
2091
|
+
* 3. A tenant-scoped store cannot bill through a personal account, and the
|
|
2092
|
+
* tenant gateway only reveals that at the first chat (`4019`). Honour the
|
|
2093
|
+
* binding when it is eligible; otherwise fall back to the preferred
|
|
2094
|
+
* tenant account, which is the one the user's enterprise tab is meant to
|
|
2095
|
+
* use.
|
|
2096
|
+
* 4. A saved account can disappear when Trae replaces its local login. Do
|
|
2097
|
+
* NOT silently fall back to a different account: that would bill a
|
|
2098
|
+
* different account than the one the user explicitly selected. Return
|
|
2099
|
+
* undefined so the caller surfaces "no signed-in account" and the user
|
|
2100
|
+
* can re-select, instead of the plugin quietly switching accounts behind
|
|
2101
|
+
* their back.
|
|
2102
|
+
*/
|
|
2103
|
+
inUse(discovered, offered) {
|
|
2104
|
+
if (this.accountId === void 0) return this.preferred(offered);
|
|
2105
|
+
const selected = discovered.find((credential) => traeAccountId(credential) === this.accountId);
|
|
2106
|
+
if (this.requireTenant && !isEnterpriseAccount(selected)) return this.preferred(offered);
|
|
2107
|
+
return selected;
|
|
2108
|
+
}
|
|
1889
2109
|
async current() {
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
return credentials.find((credential) => traeAccountId(credential) === this.accountId);
|
|
2110
|
+
const discovered = await this.readAll();
|
|
2111
|
+
return this.inUse(discovered, this.offered(discovered));
|
|
1893
2112
|
}
|
|
1894
2113
|
/**
|
|
1895
2114
|
* The stored credential of one specific account, without changing which
|
|
@@ -2637,7 +2856,19 @@ function readBody(req) {
|
|
|
2637
2856
|
});
|
|
2638
2857
|
}
|
|
2639
2858
|
function createTraeShim(options) {
|
|
2640
|
-
|
|
2859
|
+
/**
|
|
2860
|
+
* Resolve the route table. The single-client form becomes a one-entry table
|
|
2861
|
+
* labelled `solo`, so existing call sites and tests keep one code path.
|
|
2862
|
+
*/
|
|
2863
|
+
const routes = options.routes !== void 0 ? options.routes.map((route) => ({
|
|
2864
|
+
...route,
|
|
2865
|
+
secret: randomBytes(32).toString("base64url")
|
|
2866
|
+
})) : [{
|
|
2867
|
+
label: "solo",
|
|
2868
|
+
client: options.client,
|
|
2869
|
+
secret: randomBytes(32).toString("base64url")
|
|
2870
|
+
}];
|
|
2871
|
+
const secret = routes[0]?.secret ?? randomBytes(32).toString("base64url");
|
|
2641
2872
|
const sockets = /* @__PURE__ */ new Set();
|
|
2642
2873
|
const server = createServer((req, res) => {
|
|
2643
2874
|
handle(req, res);
|
|
@@ -2651,18 +2882,22 @@ function createTraeShim(options) {
|
|
|
2651
2882
|
server.once("error", reject);
|
|
2652
2883
|
});
|
|
2653
2884
|
server.listen(0, "127.0.0.1");
|
|
2654
|
-
|
|
2885
|
+
/** Route whose secret matches the request's bearer, or `undefined`. */
|
|
2886
|
+
function routeOf(req) {
|
|
2655
2887
|
const match = typeof req.headers.authorization === "string" ? /^Bearer\s+(.+)$/i.exec(req.headers.authorization.trim()) : null;
|
|
2656
|
-
if (match === null) return
|
|
2888
|
+
if (match === null) return void 0;
|
|
2657
2889
|
const actual = Buffer.from(match[1] ?? "");
|
|
2658
|
-
const
|
|
2659
|
-
|
|
2890
|
+
for (const route of routes) {
|
|
2891
|
+
const expected = Buffer.from(route.secret);
|
|
2892
|
+
if (actual.length === expected.length && timingSafeEqual(actual, expected)) return route;
|
|
2893
|
+
}
|
|
2660
2894
|
}
|
|
2661
2895
|
async function handle(req, res) {
|
|
2662
2896
|
try {
|
|
2663
2897
|
if (!hostIsLoopback(req.headers.host)) return writeError(res, 403, "host_not_allowed", "Host must be loopback");
|
|
2664
2898
|
if (!originIsLoopback(req.headers.origin)) return writeError(res, 403, "origin_not_allowed", "Origin must be loopback");
|
|
2665
|
-
|
|
2899
|
+
const route = routeOf(req);
|
|
2900
|
+
if (route === void 0) return writeError(res, 401, "unauthorized", "Missing or invalid bearer");
|
|
2666
2901
|
const url = req.url ?? "/";
|
|
2667
2902
|
if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) return writeJson(res, 200, { ok: true });
|
|
2668
2903
|
if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) return writeJson(res, 200, {
|
|
@@ -2696,7 +2931,7 @@ function createTraeShim(options) {
|
|
|
2696
2931
|
const abort = () => controller.abort();
|
|
2697
2932
|
req.once("aborted", abort);
|
|
2698
2933
|
req.socket.once("close", abort);
|
|
2699
|
-
const result = await
|
|
2934
|
+
const result = await route.client.chatStream(raw, controller.signal);
|
|
2700
2935
|
if (!result.ok) return writeError(res, STATUS_BY_KIND[result.kind], result.kind, result.message);
|
|
2701
2936
|
res.writeHead(200, {
|
|
2702
2937
|
"Content-Type": "text/event-stream",
|
|
@@ -2732,6 +2967,12 @@ function createTraeShim(options) {
|
|
|
2732
2967
|
return `http://127.0.0.1:${address.port}`;
|
|
2733
2968
|
},
|
|
2734
2969
|
token: () => secret,
|
|
2970
|
+
tokenOf(label) {
|
|
2971
|
+
if (label === void 0) return secret;
|
|
2972
|
+
const route = routes.find((entry) => entry.label === label);
|
|
2973
|
+
if (route === void 0) throw new Error(`trae shim has no route named ${label}`);
|
|
2974
|
+
return route.secret;
|
|
2975
|
+
},
|
|
2735
2976
|
close: () => new Promise((resolve, reject) => {
|
|
2736
2977
|
for (const socket of sockets) socket.destroy();
|
|
2737
2978
|
server.close((error) => error === void 0 ? resolve() : reject(error));
|
|
@@ -3080,7 +3321,7 @@ var TraeSoloUpstreamClient = class {
|
|
|
3080
3321
|
async fetchModels(signal) {
|
|
3081
3322
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
3082
3323
|
const region = regionOfCredential(credential);
|
|
3083
|
-
const base = this.options.baseUrl ??
|
|
3324
|
+
const base = this.options.baseUrl ?? this.chatBaseOf(credential);
|
|
3084
3325
|
const headers = {
|
|
3085
3326
|
...buildTraeCnHeaders(credential, identity),
|
|
3086
3327
|
Accept: "application/json"
|
|
@@ -3166,7 +3407,19 @@ var TraeSoloUpstreamClient = class {
|
|
|
3166
3407
|
}
|
|
3167
3408
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
3168
3409
|
const headers = buildTraeCnHeaders(credential, identity);
|
|
3169
|
-
const base = this.options.baseUrl ??
|
|
3410
|
+
const base = this.options.baseUrl ?? this.chatBaseOf(credential);
|
|
3411
|
+
if (this.options.route === "enterprise") try {
|
|
3412
|
+
const parsed = JSON.parse(prepared);
|
|
3413
|
+
if (parsed["session_id"] === void 0) parsed["session_id"] = randomUUID();
|
|
3414
|
+
prepared = JSON.stringify(parsed);
|
|
3415
|
+
} catch {
|
|
3416
|
+
return {
|
|
3417
|
+
ok: false,
|
|
3418
|
+
status: 400,
|
|
3419
|
+
kind: "client",
|
|
3420
|
+
message: "invalid JSON request"
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3170
3423
|
let response;
|
|
3171
3424
|
try {
|
|
3172
3425
|
response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
|
|
@@ -3183,10 +3436,36 @@ var TraeSoloUpstreamClient = class {
|
|
|
3183
3436
|
message: `transport error: ${String(error)}`
|
|
3184
3437
|
};
|
|
3185
3438
|
}
|
|
3186
|
-
if (response.ok)
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3439
|
+
if (response.ok) {
|
|
3440
|
+
/**
|
|
3441
|
+
* The enterprise gateway reports a refusal as **HTTP 200** whose first
|
|
3442
|
+
* SSE frame is `event:error`. Handing that stream back as a success would
|
|
3443
|
+
* surface the refusal as an empty assistant turn, so it is converted to
|
|
3444
|
+
* a real error here — the same shape the consumer gateway uses for its
|
|
3445
|
+
* non-2xx failures.
|
|
3446
|
+
*/
|
|
3447
|
+
if (this.options.route !== "enterprise") return {
|
|
3448
|
+
ok: true,
|
|
3449
|
+
response
|
|
3450
|
+
};
|
|
3451
|
+
const classified = await classifyEnterpriseResponse(response);
|
|
3452
|
+
if ("response" in classified) return {
|
|
3453
|
+
ok: true,
|
|
3454
|
+
response: classified.response
|
|
3455
|
+
};
|
|
3456
|
+
this.options.log?.("dsh-connect-trae: enterprise llm_utils_chat refused", {
|
|
3457
|
+
status: 200,
|
|
3458
|
+
model: JSON.parse(prepared)["model"],
|
|
3459
|
+
configName: JSON.parse(prepared)["config_name"],
|
|
3460
|
+
body: classified.refusal
|
|
3461
|
+
});
|
|
3462
|
+
return {
|
|
3463
|
+
ok: false,
|
|
3464
|
+
status: 200,
|
|
3465
|
+
kind: "client",
|
|
3466
|
+
message: classified.refusal
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3190
3469
|
const text = (await response.text()).slice(0, 1024);
|
|
3191
3470
|
this.options.log?.("dsh-connect-trae: llm_utils_chat rejected", {
|
|
3192
3471
|
status: response.status,
|
|
@@ -3202,7 +3481,77 @@ var TraeSoloUpstreamClient = class {
|
|
|
3202
3481
|
message: text || `Trae SOLO returned HTTP ${response.status}`
|
|
3203
3482
|
};
|
|
3204
3483
|
}
|
|
3484
|
+
/** Chat gateway for this client's pinned route. */
|
|
3485
|
+
chatBaseOf(credential) {
|
|
3486
|
+
if (this.options.route === "enterprise") return TRAE_ENTERPRISE_HOST;
|
|
3487
|
+
return REGION_GATEWAYS[regionOfCredential(credential)].chat;
|
|
3488
|
+
}
|
|
3205
3489
|
};
|
|
3490
|
+
/**
|
|
3491
|
+
* Return the refusal frame of an enterprise 200, or `undefined` when the stream
|
|
3492
|
+
* is a real answer.
|
|
3493
|
+
*
|
|
3494
|
+
* Reads (and re-plays) only the first SSE frame: a refusal is emitted before
|
|
3495
|
+
* any output token, so peeking that far is enough to classify the call, and
|
|
3496
|
+
* the stream is rebuilt so a caller that got `undefined` still sees every byte.
|
|
3497
|
+
*/
|
|
3498
|
+
function concatStream(head, reader) {
|
|
3499
|
+
return new ReadableStream({ start(controller) {
|
|
3500
|
+
if (head !== "") controller.enqueue(new TextEncoder().encode(head));
|
|
3501
|
+
const drain = () => {
|
|
3502
|
+
reader.read().then((result) => {
|
|
3503
|
+
if (result.done) {
|
|
3504
|
+
controller.close();
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
controller.enqueue(result.value);
|
|
3508
|
+
drain();
|
|
3509
|
+
}).catch((error) => controller.error(error));
|
|
3510
|
+
};
|
|
3511
|
+
drain();
|
|
3512
|
+
} });
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Classify an enterprise 200.
|
|
3516
|
+
*
|
|
3517
|
+
* Returns `{ refusal }` when the first SSE frame is `event:error`, otherwise
|
|
3518
|
+
* `{ response }` carrying a REBUILT response whose body re-emits every byte —
|
|
3519
|
+
* including the peeked frame. A refusal is emitted before any output token, so
|
|
3520
|
+
* peeking one frame is enough to classify the call.
|
|
3521
|
+
*
|
|
3522
|
+
* The response is rebuilt rather than patched: the original's body has been
|
|
3523
|
+
* partially read, so its `text()`/`json()` would keep failing with "Body has
|
|
3524
|
+
* already been read" even after `body` is swapped.
|
|
3525
|
+
*/
|
|
3526
|
+
async function classifyEnterpriseResponse(response) {
|
|
3527
|
+
const body = response.body;
|
|
3528
|
+
if (body === null) return { response };
|
|
3529
|
+
const reader = body.getReader();
|
|
3530
|
+
const decoder = new TextDecoder();
|
|
3531
|
+
let head = "";
|
|
3532
|
+
try {
|
|
3533
|
+
while (!head.includes("\n\n")) {
|
|
3534
|
+
const { done, value } = await reader.read();
|
|
3535
|
+
if (done) break;
|
|
3536
|
+
head += decoder.decode(value, { stream: true });
|
|
3537
|
+
if (head.length > 8192) break;
|
|
3538
|
+
}
|
|
3539
|
+
} catch {
|
|
3540
|
+
return { response };
|
|
3541
|
+
}
|
|
3542
|
+
const frame = head.split("\n\n")[0] ?? "";
|
|
3543
|
+
const event = /^event:(\S+)/m.exec(frame)?.[1];
|
|
3544
|
+
const data = frame.split("\n").find((line) => line.startsWith("data:"))?.slice(5).trim() ?? "";
|
|
3545
|
+
if (event !== "error") return { response: new Response(concatStream(head, reader), {
|
|
3546
|
+
status: response.status,
|
|
3547
|
+
statusText: response.statusText,
|
|
3548
|
+
headers: response.headers
|
|
3549
|
+
}) };
|
|
3550
|
+
try {
|
|
3551
|
+
await reader.cancel();
|
|
3552
|
+
} catch {}
|
|
3553
|
+
return { refusal: data === "" ? "enterprise gateway refused the request" : data };
|
|
3554
|
+
}
|
|
3206
3555
|
//#endregion
|
|
3207
3556
|
//#region src/sse.ts
|
|
3208
3557
|
/** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
|
|
@@ -3343,6 +3692,13 @@ function decodeTraeEvent(event) {
|
|
|
3343
3692
|
* two different statements, and a user hits the second without touching the
|
|
3344
3693
|
* first (issue #33).
|
|
3345
3694
|
* - `4023` — the model name is unknown on the route that was asked.
|
|
3695
|
+
* - `4222` — the ENTERPRISE tenant's allowance is exhausted. Measured
|
|
3696
|
+
* 2026-10-17 on a live tenant account whose enterprise credit ran out: the
|
|
3697
|
+
* gateway answered HTTP 200 with `{"code":4222,"message":""}` for a model
|
|
3698
|
+
* that streamed normally moments earlier (and still streams on the consumer
|
|
3699
|
+
* gateway). Like `4008` it is a billing fact rather than a config one, but
|
|
3700
|
+
* it is specific to the tenant pool -- the consumer balance is unaffected,
|
|
3701
|
+
* which is why it must not be folded into the `4008` reading.
|
|
3346
3702
|
* - `2001` — the upstream has not published its function config for the route
|
|
3347
3703
|
* that was asked. Evidence is issue #39: on a live CN account every SOLO
|
|
3348
3704
|
* function's `get_detail_param` answered HTTP 200 with `config_info_list:
|
|
@@ -3360,7 +3716,8 @@ const TRAE_ERROR_HINTS = {
|
|
|
3360
3716
|
4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
|
|
3361
3717
|
4008: "Trae reports the account's SOLO quota is exhausted for this request",
|
|
3362
3718
|
4011: "Trae throttled the SOLO channel for this account (a rate limit, not the credit balance)",
|
|
3363
|
-
4023: "Trae does not recognise this model name on the SOLO route that was asked"
|
|
3719
|
+
4023: "Trae does not recognise this model name on the SOLO route that was asked",
|
|
3720
|
+
4222: "The enterprise tenant allowance is exhausted (tenant spend is separate from the personal credit balance; the consumer gateway still has its own)"
|
|
3364
3721
|
};
|
|
3365
3722
|
/** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
|
|
3366
3723
|
function traeErrorPlan(extra) {
|
|
@@ -3690,14 +4047,29 @@ var TraeSoloBridge = class {
|
|
|
3690
4047
|
try {
|
|
3691
4048
|
const input = JSON.parse(bodyJson);
|
|
3692
4049
|
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
|
|
3693
|
-
const
|
|
4050
|
+
const entryKey = model.trim().toLowerCase();
|
|
4051
|
+
const entry = this.catalog?.current().find((item) => item.id === model || item.id.trim().toLowerCase() === entryKey);
|
|
3694
4052
|
const fromCatalog = entry?.wireConfigName === void 0 ? void 0 : {
|
|
3695
4053
|
configName: entry.wireConfigName,
|
|
3696
4054
|
...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
|
|
3697
4055
|
};
|
|
3698
4056
|
const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
|
|
3699
4057
|
const target = fromCatalog ?? fromResolver;
|
|
3700
|
-
|
|
4058
|
+
/**
|
|
4059
|
+
* Last line of defence: the discovered row's own id.
|
|
4060
|
+
*
|
|
4061
|
+
* For models whose directory id IS the gateway `config_name` the merge
|
|
4062
|
+
* deliberately emits no `wireConfigName` (there is nothing to map), so a
|
|
4063
|
+
* requested id that differs only by case has neither mapping source —
|
|
4064
|
+
* and the row we just matched case-blindly is exactly the canonical
|
|
4065
|
+
* spelling. Sending it is what a correct mapping would have produced.
|
|
4066
|
+
*
|
|
4067
|
+
* Deliberately NOT derived from the row's `name`: the rosters spell most
|
|
4068
|
+
* ids in the opposite case of their display names (`glm-5.3` vs
|
|
4069
|
+
* `GLM-5.3`) and the gateway accepts the LOWERCASE id for those — a
|
|
4070
|
+
* name-derived rewrite answered `4001` for 4 of 5 probed models.
|
|
4071
|
+
*/
|
|
4072
|
+
const wireModel = target?.configName ?? entry?.id ?? model;
|
|
3701
4073
|
const wireFunction = target?.function ?? entry?.wireFunction;
|
|
3702
4074
|
if (wireModel !== input["model"]) input["model"] = wireModel;
|
|
3703
4075
|
if (wireFunction !== void 0 && input["function"] !== wireFunction) input["function"] = wireFunction;
|
|
@@ -5442,16 +5814,26 @@ function toCheckin(status) {
|
|
|
5442
5814
|
};
|
|
5443
5815
|
}
|
|
5444
5816
|
/**
|
|
5445
|
-
* Assemble one region's card document. `
|
|
5446
|
-
*
|
|
5817
|
+
* Assemble one region's card document. `panel` is the tab the card is on; the
|
|
5818
|
+
* panel-scoped store already answers with only that region's accounts, so the
|
|
5447
5819
|
* document's model slots and account list are that region's by construction.
|
|
5448
5820
|
* Sign-in state is read-only; credit is a live billing answer whose failure
|
|
5449
5821
|
* degrades to `creditsError` rather than failing the whole document.
|
|
5450
5822
|
*/
|
|
5451
|
-
async function traeWebUsage(deps,
|
|
5452
|
-
const store = deps.store(
|
|
5453
|
-
const enabled = deps.regionEnabled(
|
|
5454
|
-
|
|
5823
|
+
async function traeWebUsage(deps, panel) {
|
|
5824
|
+
const store = deps.store(panel);
|
|
5825
|
+
const enabled = deps.regionEnabled(panel);
|
|
5826
|
+
/**
|
|
5827
|
+
* Accounts this panel may actually bill.
|
|
5828
|
+
*
|
|
5829
|
+
* Filtered ONCE, at the source, because this list is spliced into every
|
|
5830
|
+
* response shape below (`signed-out`, `signed-in`, the credits route). The
|
|
5831
|
+
* enterprise pool can only bill a tenant account, and the tenant gateway does
|
|
5832
|
+
* not make that obvious: it answers a personal token's DIRECTORY read in
|
|
5833
|
+
* full and only refuses its chat (`4019`). Listing a personal account there
|
|
5834
|
+
* would offer a binding that cannot work.
|
|
5835
|
+
*/
|
|
5836
|
+
const accounts = (await store.accounts()).filter((account) => panel !== "enterprise" || account.enterprise);
|
|
5455
5837
|
if ((await store.status()).state !== "signed-in") {
|
|
5456
5838
|
const { failures } = await store.diagnose();
|
|
5457
5839
|
return {
|
|
@@ -5480,26 +5862,26 @@ async function traeWebUsage(deps, region) {
|
|
|
5480
5862
|
}
|
|
5481
5863
|
let appLogin;
|
|
5482
5864
|
try {
|
|
5483
|
-
appLogin = await deps.liveLogin?.(
|
|
5865
|
+
appLogin = await deps.liveLogin?.(panel);
|
|
5484
5866
|
} catch {}
|
|
5485
5867
|
const account = {
|
|
5486
5868
|
accountId: accounts.find((item) => item.selected)?.id ?? "",
|
|
5487
5869
|
accountName: credential.accountName ?? credential.userId,
|
|
5488
5870
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
5489
5871
|
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
5490
|
-
region,
|
|
5872
|
+
region: regionOfPanel(panel),
|
|
5491
5873
|
enabled,
|
|
5492
5874
|
accounts,
|
|
5493
5875
|
...appLogin === void 0 ? {} : { appLogin },
|
|
5494
|
-
models: deps.displayModels(
|
|
5876
|
+
models: deps.displayModels(panel).map((model) => ({
|
|
5495
5877
|
...model,
|
|
5496
5878
|
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
5497
5879
|
})),
|
|
5498
|
-
enabledModelIds: [...deps.enabledModelIds(
|
|
5499
|
-
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(
|
|
5880
|
+
enabledModelIds: [...deps.enabledModelIds(panel)],
|
|
5881
|
+
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
|
|
5500
5882
|
};
|
|
5501
|
-
const client = deps.client(
|
|
5502
|
-
if (
|
|
5883
|
+
const client = deps.client(panel);
|
|
5884
|
+
if (regionOfPanel(panel) === "ai") try {
|
|
5503
5885
|
const payStatus = await client.payStatus();
|
|
5504
5886
|
return {
|
|
5505
5887
|
status: "signed-in",
|
|
@@ -5513,9 +5895,25 @@ async function traeWebUsage(deps, region) {
|
|
|
5513
5895
|
payStatusError: safeMessage(error)
|
|
5514
5896
|
};
|
|
5515
5897
|
}
|
|
5898
|
+
/**
|
|
5899
|
+
* The enterprise panel shows NO credit figure, deliberately.
|
|
5900
|
+
*
|
|
5901
|
+
* `client.snapshot()` reads the CONSUMER packs, and that is the personal
|
|
5902
|
+
* balance — not what a tenant turn spends. There is no enterprise counterpart
|
|
5903
|
+
* to read: every tenant quota endpoint answers 404 (probed 2026-10-17), and
|
|
5904
|
+
* the tenant console at console.enterprise.trae.cn is the only place the
|
|
5905
|
+
* enterprise balance exists. Showing the personal number under an enterprise
|
|
5906
|
+
* heading would be actively misleading, so the panel reports the absence
|
|
5907
|
+
* explicitly and the card explains where the real figure lives.
|
|
5908
|
+
*/
|
|
5909
|
+
if (panel === "enterprise") return {
|
|
5910
|
+
status: "signed-in",
|
|
5911
|
+
...account,
|
|
5912
|
+
creditsUnavailable: "enterprise"
|
|
5913
|
+
};
|
|
5516
5914
|
const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), client.checkinStatus()]);
|
|
5517
5915
|
const credits = snapshotResult.status === "fulfilled" ? toCredits(snapshotResult.value) : void 0;
|
|
5518
|
-
const alternatives = credits !== void 0 && credits.workAvailable <= 0 && credits.generalAvailable <= 0 ? await creditAlternatives(deps,
|
|
5916
|
+
const alternatives = credits !== void 0 && credits.workAvailable <= 0 && credits.generalAvailable <= 0 ? await creditAlternatives(deps, panel, accounts) : void 0;
|
|
5519
5917
|
return {
|
|
5520
5918
|
status: "signed-in",
|
|
5521
5919
|
...account,
|
|
@@ -5536,13 +5934,13 @@ async function traeWebUsage(deps, region) {
|
|
|
5536
5934
|
* problem to explain. The list is capped so a machine with many stale
|
|
5537
5935
|
* credentials cannot turn one status read into a burst of upstream calls.
|
|
5538
5936
|
*/
|
|
5539
|
-
async function creditAlternatives(deps,
|
|
5937
|
+
async function creditAlternatives(deps, panel, accounts) {
|
|
5540
5938
|
if (deps.creditsOfAccount === void 0) return void 0;
|
|
5541
|
-
const candidates = accounts.filter((account) => !account.selected && account.region ===
|
|
5939
|
+
const candidates = accounts.filter((account) => !account.selected && account.region === regionOfPanel(panel)).filter((account) => panel !== "enterprise" || account.enterprise).slice(0, TRAE_CREDIT_ALTERNATIVE_LIMIT);
|
|
5542
5940
|
if (candidates.length === 0) return void 0;
|
|
5543
5941
|
const alternatives = (await Promise.all(candidates.map(async (account) => {
|
|
5544
5942
|
try {
|
|
5545
|
-
const credits = await deps.creditsOfAccount(
|
|
5943
|
+
const credits = await deps.creditsOfAccount(panel, account.id);
|
|
5546
5944
|
if (credits === void 0) return void 0;
|
|
5547
5945
|
if (credits.workAvailable <= 0 && credits.generalAvailable <= 0) return void 0;
|
|
5548
5946
|
return {
|
|
@@ -5559,11 +5957,11 @@ async function creditAlternatives(deps, region, accounts) {
|
|
|
5559
5957
|
return alternatives.length === 0 ? void 0 : alternatives;
|
|
5560
5958
|
}
|
|
5561
5959
|
/**
|
|
5562
|
-
* The
|
|
5960
|
+
* The panel a request addresses, or a 400 answer. Absent parameter means the
|
|
5563
5961
|
* domestic tab; an unknown value is refused rather than guessed.
|
|
5564
5962
|
*/
|
|
5565
5963
|
/**
|
|
5566
|
-
* The general balance of every account in one
|
|
5964
|
+
* The general balance of every account in one panel.
|
|
5567
5965
|
*
|
|
5568
5966
|
* One upstream read per account — the same call `creditsOfAccount` already
|
|
5569
5967
|
* makes for the exhausted-balance alternatives, widened to every account and to
|
|
@@ -5574,16 +5972,16 @@ async function creditAlternatives(deps, region, accounts) {
|
|
|
5574
5972
|
* figure: the table lists how many accounts exist even when one cannot be
|
|
5575
5973
|
* quoted, rather than dropping it and making the switch unavailable.
|
|
5576
5974
|
*/
|
|
5577
|
-
async function traeAccountCredits(deps,
|
|
5578
|
-
const accounts = (await deps.store(
|
|
5975
|
+
async function traeAccountCredits(deps, panel) {
|
|
5976
|
+
const accounts = (await deps.store(panel).accounts()).filter((account) => account.region === regionOfPanel(panel)).filter((account) => panel !== "enterprise" || account.enterprise);
|
|
5579
5977
|
if (accounts.length === 0) return [];
|
|
5580
5978
|
return Promise.all(accounts.map(async (account) => {
|
|
5581
5979
|
let generalAvailable;
|
|
5582
5980
|
if (account.selected) try {
|
|
5583
|
-
generalAvailable = toCredits(await deps.client(
|
|
5981
|
+
generalAvailable = toCredits(await deps.client(panel).snapshot()).generalAvailable;
|
|
5584
5982
|
} catch {}
|
|
5585
5983
|
else if (deps.creditsOfAccount !== void 0) try {
|
|
5586
|
-
generalAvailable = (await deps.creditsOfAccount(
|
|
5984
|
+
generalAvailable = (await deps.creditsOfAccount(panel, account.id))?.generalAvailable;
|
|
5587
5985
|
} catch {}
|
|
5588
5986
|
return {
|
|
5589
5987
|
id: account.id,
|
|
@@ -5594,12 +5992,12 @@ async function traeAccountCredits(deps, region) {
|
|
|
5594
5992
|
}));
|
|
5595
5993
|
}
|
|
5596
5994
|
function requestRegion(req, res) {
|
|
5597
|
-
const
|
|
5598
|
-
if (
|
|
5995
|
+
const panel = regionOfTraeStatusUrl(req.url ?? "/");
|
|
5996
|
+
if (panel === void 0) {
|
|
5599
5997
|
json(res, 400, { error: "unknown region" });
|
|
5600
5998
|
return;
|
|
5601
5999
|
}
|
|
5602
|
-
return
|
|
6000
|
+
return panel;
|
|
5603
6001
|
}
|
|
5604
6002
|
/** Mount the GET usage route on an optional webServer context. */
|
|
5605
6003
|
function registerTraeUsageRoute(ctx, deps) {
|
|
@@ -5616,17 +6014,17 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5616
6014
|
json(res, 403, { error: "origin-not-trusted" });
|
|
5617
6015
|
return;
|
|
5618
6016
|
}
|
|
5619
|
-
const
|
|
5620
|
-
if (
|
|
6017
|
+
const panel = requestRegion(req, res);
|
|
6018
|
+
if (panel === void 0) return;
|
|
5621
6019
|
try {
|
|
5622
|
-
json(res, 200, await traeWebUsage(deps,
|
|
6020
|
+
json(res, 200, await traeWebUsage(deps, panel));
|
|
5623
6021
|
} catch (error) {
|
|
5624
6022
|
json(res, 500, { error: safeMessage(error) });
|
|
5625
6023
|
}
|
|
5626
6024
|
}
|
|
5627
6025
|
});
|
|
5628
6026
|
/**
|
|
5629
|
-
* General balance of every account in the
|
|
6027
|
+
* General balance of every account in the panel — read only when the
|
|
5630
6028
|
* composer panel is opened. One upstream read per account, paid once per
|
|
5631
6029
|
* open rather than on the 5-minute cycle (see
|
|
5632
6030
|
* {@link TRAE_ACCOUNT_CREDITS_PATH}).
|
|
@@ -5637,10 +6035,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5637
6035
|
handler: async (req, res) => {
|
|
5638
6036
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
5639
6037
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5640
|
-
const
|
|
5641
|
-
if (
|
|
6038
|
+
const panel = requestRegion(req, res);
|
|
6039
|
+
if (panel === void 0) return;
|
|
5642
6040
|
try {
|
|
5643
|
-
json(res, 200, { accounts: await traeAccountCredits(deps,
|
|
6041
|
+
json(res, 200, { accounts: await traeAccountCredits(deps, panel) });
|
|
5644
6042
|
} catch (error) {
|
|
5645
6043
|
json(res, 500, { error: safeMessage(error) });
|
|
5646
6044
|
}
|
|
@@ -5660,12 +6058,12 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5660
6058
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
5661
6059
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5662
6060
|
if (deps.accountStateOf === void 0) return json(res, 503, { error: "account status unavailable" });
|
|
5663
|
-
const
|
|
5664
|
-
if (
|
|
6061
|
+
const panel = requestRegion(req, res);
|
|
6062
|
+
if (panel === void 0) return;
|
|
5665
6063
|
const accountId = new URL(req.url ?? "/", "http://localhost").searchParams.get("accountId");
|
|
5666
6064
|
if (accountId === null || accountId.trim() === "") return json(res, 400, { error: "missing accountId" });
|
|
5667
6065
|
try {
|
|
5668
|
-
const state = await deps.accountStateOf(
|
|
6066
|
+
const state = await deps.accountStateOf(panel, accountId);
|
|
5669
6067
|
if (state === void 0) return json(res, 404, { error: "unknown account" });
|
|
5670
6068
|
json(res, 200, state);
|
|
5671
6069
|
} catch (error) {
|
|
@@ -5679,10 +6077,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5679
6077
|
handler: async (req, res) => {
|
|
5680
6078
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5681
6079
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5682
|
-
const
|
|
5683
|
-
if (
|
|
6080
|
+
const panel = requestRegion(req, res);
|
|
6081
|
+
if (panel === void 0) return;
|
|
5684
6082
|
try {
|
|
5685
|
-
json(res, 200, { accounts: await deps.store(
|
|
6083
|
+
json(res, 200, { accounts: (await deps.store(panel).accounts()).filter((account) => panel !== "enterprise" || account.enterprise) });
|
|
5686
6084
|
} catch (error) {
|
|
5687
6085
|
json(res, 500, { error: safeMessage(error) });
|
|
5688
6086
|
}
|
|
@@ -5703,15 +6101,15 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5703
6101
|
handler: async (req, res) => {
|
|
5704
6102
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5705
6103
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5706
|
-
const
|
|
5707
|
-
if (
|
|
6104
|
+
const panel = requestRegion(req, res);
|
|
6105
|
+
if (panel === void 0) return;
|
|
5708
6106
|
const action = transferActionOf(req.url ?? "");
|
|
5709
6107
|
if (action === void 0) return json(res, 400, { error: "unknown transfer action" });
|
|
5710
6108
|
try {
|
|
5711
6109
|
const body = await readTransferBody(req);
|
|
5712
6110
|
if (action === "export") {
|
|
5713
6111
|
const wanted = new Set(Array.isArray(body["accountIds"]) ? body["accountIds"].filter((id) => typeof id === "string") : []);
|
|
5714
|
-
const store = deps.store(
|
|
6112
|
+
const store = deps.store(panel);
|
|
5715
6113
|
const records = [];
|
|
5716
6114
|
for (const account of await store.accounts()) {
|
|
5717
6115
|
if (!wanted.has(account.id)) continue;
|
|
@@ -5738,8 +6136,8 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5738
6136
|
const text = typeof body["text"] === "string" ? body["text"] : "";
|
|
5739
6137
|
if (action === "preview") return json(res, 200, previewTransferFile(text));
|
|
5740
6138
|
const indexes = Array.isArray(body["indexes"]) ? body["indexes"].filter((index) => typeof index === "number" && Number.isInteger(index)) : [];
|
|
5741
|
-
const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes,
|
|
5742
|
-
if (changes.size > 0) await deps.store(
|
|
6139
|
+
const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, regionOfPanel(panel));
|
|
6140
|
+
if (changes.size > 0) await deps.store(panel).importAccounts([...changes.values()]);
|
|
5743
6141
|
json(res, 200, {
|
|
5744
6142
|
imported,
|
|
5745
6143
|
skipped
|
|
@@ -5756,10 +6154,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5756
6154
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5757
6155
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5758
6156
|
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
5759
|
-
const
|
|
5760
|
-
if (
|
|
6157
|
+
const panel = requestRegion(req, res);
|
|
6158
|
+
if (panel === void 0) return;
|
|
5761
6159
|
try {
|
|
5762
|
-
json(res, 200, { models: await deps.discoverModels(
|
|
6160
|
+
json(res, 200, { models: await deps.discoverModels(panel) });
|
|
5763
6161
|
} catch (error) {
|
|
5764
6162
|
json(res, 500, { error: safeMessage(error) });
|
|
5765
6163
|
}
|
|
@@ -5782,8 +6180,8 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5782
6180
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5783
6181
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5784
6182
|
if (deps.probeModels === void 0) return json(res, 503, { error: "model test unavailable" });
|
|
5785
|
-
const
|
|
5786
|
-
if (
|
|
6183
|
+
const panel = requestRegion(req, res);
|
|
6184
|
+
if (panel === void 0) return;
|
|
5787
6185
|
let ids;
|
|
5788
6186
|
try {
|
|
5789
6187
|
ids = await readModelTestBody(req);
|
|
@@ -5793,7 +6191,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5793
6191
|
if (ids.length === 0) return json(res, 400, { error: "no models to test" });
|
|
5794
6192
|
if (ids.length > 64) return json(res, 400, { error: `too many models (limit ${String(64)})` });
|
|
5795
6193
|
try {
|
|
5796
|
-
json(res, 200, await deps.probeModels(
|
|
6194
|
+
json(res, 200, await deps.probeModels(panel, ids));
|
|
5797
6195
|
} catch (error) {
|
|
5798
6196
|
json(res, 500, { error: safeMessage(error) });
|
|
5799
6197
|
}
|
|
@@ -5825,9 +6223,9 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5825
6223
|
handler: async (req, res) => {
|
|
5826
6224
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5827
6225
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5828
|
-
const
|
|
5829
|
-
if (
|
|
5830
|
-
if (
|
|
6226
|
+
const panel = requestRegion(req, res);
|
|
6227
|
+
if (panel === void 0) return;
|
|
6228
|
+
if (regionOfPanel(panel) !== "cn") return json(res, 404, { error: "check-in is not available for the international region" });
|
|
5831
6229
|
try {
|
|
5832
6230
|
let accountId;
|
|
5833
6231
|
try {
|
|
@@ -5837,7 +6235,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5837
6235
|
}
|
|
5838
6236
|
if (accountId !== void 0) {
|
|
5839
6237
|
if (deps.claimCheckinOf === void 0) return json(res, 503, { error: "check-in by account unavailable" });
|
|
5840
|
-
const outcome = await deps.claimCheckinOf(
|
|
6238
|
+
const outcome = await deps.claimCheckinOf(panel, accountId);
|
|
5841
6239
|
if (outcome === void 0) return json(res, 404, { error: "unknown account" });
|
|
5842
6240
|
const { claim, checkin: after } = outcome;
|
|
5843
6241
|
const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
|
|
@@ -5850,7 +6248,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
5850
6248
|
checkin: toCheckin(after)
|
|
5851
6249
|
});
|
|
5852
6250
|
}
|
|
5853
|
-
const client = deps.client(
|
|
6251
|
+
const client = deps.client(panel);
|
|
5854
6252
|
const current = await client.checkinStatus();
|
|
5855
6253
|
if (!current.enabled) return json(res, 409, { error: "check-in is not enabled for this account" });
|
|
5856
6254
|
if (current.checkedIn) return json(res, 200, {
|
|
@@ -6119,18 +6517,24 @@ function settingsNamespaceOf(ctx) {
|
|
|
6119
6517
|
return typeof id === "string" && id !== "" ? id : TRAE_SETTINGS_NS;
|
|
6120
6518
|
}
|
|
6121
6519
|
/**
|
|
6122
|
-
* One
|
|
6520
|
+
* One panel's saved state. A config written before the region split has only
|
|
6123
6521
|
* the flat fields: those were always captured from the CN endpoint (the
|
|
6124
6522
|
* plugin had no international support), so they are read as the CN state and
|
|
6125
6523
|
* only when no explicit CN slot exists. The ai region never inherits them —
|
|
6126
6524
|
* that inheritance is exactly the bug where a stale CN directory would be
|
|
6127
6525
|
* intersected with the international catalog and silently drop the user's
|
|
6128
6526
|
* picks (the same failure workbuddy fixed with its region split).
|
|
6527
|
+
*
|
|
6528
|
+
* Keyed by PANEL (`cn` | `enterprise` | `ai`), because each panel carries its
|
|
6529
|
+
* own switch, account binding and model selection. `enterprise` has no legacy
|
|
6530
|
+
* equivalent: it starts from an EMPTY state (its own directory, its own model
|
|
6531
|
+
* picks) and never inherits CN's, so switching it on cannot silently activate
|
|
6532
|
+
* a model list the user never chose for the tenant pool.
|
|
6129
6533
|
*/
|
|
6130
|
-
function regionStateOf(config,
|
|
6131
|
-
const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[
|
|
6534
|
+
function regionStateOf(config, panel) {
|
|
6535
|
+
const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[panel]);
|
|
6132
6536
|
if (stored !== void 0) return stored;
|
|
6133
|
-
if (
|
|
6537
|
+
if (panel !== "cn") return {};
|
|
6134
6538
|
return {
|
|
6135
6539
|
...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
|
|
6136
6540
|
...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
|
|
@@ -6139,15 +6543,23 @@ function regionStateOf(config, region) {
|
|
|
6139
6543
|
};
|
|
6140
6544
|
}
|
|
6141
6545
|
/**
|
|
6142
|
-
* Whether one
|
|
6546
|
+
* Whether one panel's provider is switched on (issue #11). Opt-out semantics:
|
|
6143
6547
|
* only an explicit `false` disables it, so every config written before this
|
|
6144
6548
|
* switch existed — including the pre-region-split flat fields, which never
|
|
6145
6549
|
* carry `enabled` — keeps both providers running exactly as before. The card
|
|
6146
6550
|
* reads the same rule through `regionEnabledOf`, so the two halves can never
|
|
6147
6551
|
* disagree about a region's state.
|
|
6552
|
+
*
|
|
6553
|
+
* The ENTERPRISE panel is the one deliberate exception: it defaults OFF. A
|
|
6554
|
+
* consumer-only account (`tenant 7o2d894p7dr0o4`) cannot use that pool at all,
|
|
6555
|
+
* so opting every existing user into a second CN provider would put a route in
|
|
6556
|
+
* their model picker that fails on first use. It is switched on from its card
|
|
6557
|
+
* tab, by someone whose account actually belongs to a tenant.
|
|
6148
6558
|
*/
|
|
6149
|
-
function regionEnabled(config,
|
|
6150
|
-
|
|
6559
|
+
function regionEnabled(config, panel) {
|
|
6560
|
+
const stored = regionStateOf(config, panel).enabled;
|
|
6561
|
+
if (stored !== void 0) return stored !== false;
|
|
6562
|
+
return panel !== "enterprise";
|
|
6151
6563
|
}
|
|
6152
6564
|
/**
|
|
6153
6565
|
* WHERE one region's credit readout sits (learned from workbuddy #62).
|
|
@@ -6189,10 +6601,16 @@ const modelConfig = z.object({
|
|
|
6189
6601
|
input: z.array(z.union(["text", "image"])),
|
|
6190
6602
|
creditMultiplier: z.number(),
|
|
6191
6603
|
wireFunction: z.string(),
|
|
6604
|
+
wireConfigName: z.string(),
|
|
6192
6605
|
multimodal: z.boolean()
|
|
6193
6606
|
});
|
|
6607
|
+
/**
|
|
6608
|
+
* One panel's saved state. Panels are keyed `cn | enterprise | ai` — see
|
|
6609
|
+
* {@link TraePanel}; `enterprise` is a second CN provider with its own switch,
|
|
6610
|
+
* account binding and model selection.
|
|
6611
|
+
*/
|
|
6194
6612
|
const regionStateConfig = z.object({
|
|
6195
|
-
enabled: z.boolean().
|
|
6613
|
+
enabled: z.boolean().description("Whether this panel's provider is offered to DSH (false withdraws it entirely)"),
|
|
6196
6614
|
lastCatalog: z.array(modelConfig).default([]),
|
|
6197
6615
|
enabledModelIds: z.array(z.string()).default([]),
|
|
6198
6616
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -6201,7 +6619,8 @@ const regionStateConfig = z.object({
|
|
|
6201
6619
|
});
|
|
6202
6620
|
const accountSelectionConfig = z.object({
|
|
6203
6621
|
cn: z.string().description("Selected domestic (CN) account id (never a token)"),
|
|
6204
|
-
ai: z.string().description("Selected international account id (never a token)")
|
|
6622
|
+
ai: z.string().description("Selected international account id (never a token)"),
|
|
6623
|
+
enterprise: z.string().description("Selected account id for the CN enterprise pool (never a token)")
|
|
6205
6624
|
});
|
|
6206
6625
|
const Config = z.object({
|
|
6207
6626
|
authFile: asVolatile(z.string().description("Optional Trae storage.json path override")),
|
|
@@ -6215,8 +6634,8 @@ const Config = z.object({
|
|
|
6215
6634
|
accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
|
|
6216
6635
|
showPointsInMainUi: asVolatile(z.boolean().default(false).description("在输入框工具栏显示积分读数「Trae CN · <数值>」(仅当选中的模型属于本插件时出现,可点击查看账号与积分明细)")),
|
|
6217
6636
|
followAppLogin: asVolatile(z.boolean().default(false).description("跟随 Trae 桌面端当前登录账号换绑(打开后每次配置生效时重绑到 App 现在登录的账号)")),
|
|
6218
|
-
accounts: asVolatile(accountSelectionConfig.description("Per-
|
|
6219
|
-
regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-
|
|
6637
|
+
accounts: asVolatile(accountSelectionConfig.description("Per-panel account selections, keyed cn | enterprise | ai")),
|
|
6638
|
+
regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-panel model directory and selection, keyed cn | enterprise | ai")),
|
|
6220
6639
|
lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
|
|
6221
6640
|
enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
|
|
6222
6641
|
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
|
|
@@ -6228,8 +6647,8 @@ const REGION_KEYS = ["cn", "ai"];
|
|
|
6228
6647
|
function apply(ctx, config) {
|
|
6229
6648
|
const settingsNs = settingsNamespaceOf(ctx);
|
|
6230
6649
|
let current = () => config;
|
|
6231
|
-
const enabledSet = (value,
|
|
6232
|
-
const imageSet = (value,
|
|
6650
|
+
const enabledSet = (value, panel) => new Set(regionStateOf(value, panel).enabledModelIds ?? []);
|
|
6651
|
+
const imageSet = (value, panel) => new Set(regionStateOf(value, panel).imageModelIds ?? []);
|
|
6233
6652
|
/**
|
|
6234
6653
|
* Per-region wire state. Each region's discovery owns its own maps: the CN
|
|
6235
6654
|
* and international rosters overlap (and spell some ids differently), so a
|
|
@@ -6242,30 +6661,30 @@ function apply(ctx, config) {
|
|
|
6242
6661
|
* `lastCatalog` / `models` / `enabledModelIds` that still lists it.
|
|
6243
6662
|
*/
|
|
6244
6663
|
const wireState = {};
|
|
6245
|
-
for (const
|
|
6664
|
+
for (const panel of TRAE_PANELS) wireState[panel] = {
|
|
6246
6665
|
callableKeys: /* @__PURE__ */ new Set(),
|
|
6247
6666
|
resolved: false,
|
|
6248
6667
|
byId: /* @__PURE__ */ new Map(),
|
|
6249
6668
|
byName: /* @__PURE__ */ new Map()
|
|
6250
6669
|
};
|
|
6251
|
-
const dropDeadModels = (rows,
|
|
6252
|
-
const wire = wireState[
|
|
6670
|
+
const dropDeadModels = (rows, panel) => {
|
|
6671
|
+
const wire = wireState[panel];
|
|
6253
6672
|
if (!wire.resolved) return rows;
|
|
6254
6673
|
return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
|
|
6255
6674
|
};
|
|
6256
|
-
const fallbackModels = (value,
|
|
6257
|
-
const derive = (value, raw,
|
|
6258
|
-
const selectedImages = imageSet(value,
|
|
6259
|
-
const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw,
|
|
6260
|
-
return derived.length > 0 ? derived : fallbackModels(value,
|
|
6675
|
+
const fallbackModels = (value, panel) => applyImageSelection(fallbackModelsFor(regionOfPanel(panel)), imageSet(value, panel));
|
|
6676
|
+
const derive = (value, raw, panel) => {
|
|
6677
|
+
const selectedImages = imageSet(value, panel);
|
|
6678
|
+
const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, panel)), selectedImages), enabledSet(value, panel), regionStateOf(value, panel).contextBudgets ?? {});
|
|
6679
|
+
return derived.length > 0 ? derived : fallbackModels(value, panel);
|
|
6261
6680
|
};
|
|
6262
|
-
const configuredModels = (value,
|
|
6263
|
-
const state = regionStateOf(value,
|
|
6264
|
-
return state.lastCatalog?.length ? derive(value, state.lastCatalog,
|
|
6681
|
+
const configuredModels = (value, panel) => {
|
|
6682
|
+
const state = regionStateOf(value, panel);
|
|
6683
|
+
return state.lastCatalog?.length ? derive(value, state.lastCatalog, panel) : panel === "cn" && value.models?.length ? derive(value, value.models, panel) : fallbackModels(value, panel);
|
|
6265
6684
|
};
|
|
6266
|
-
const displayModels = (value,
|
|
6267
|
-
const state = regionStateOf(value,
|
|
6268
|
-
return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog),
|
|
6685
|
+
const displayModels = (value, panel) => {
|
|
6686
|
+
const state = regionStateOf(value, panel);
|
|
6687
|
+
return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), panel) : panel === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), panel) : fallbackModelsFor(regionOfPanel(panel));
|
|
6269
6688
|
};
|
|
6270
6689
|
/**
|
|
6271
6690
|
* Legacy migration for the pre-split single `accountId`: its region is
|
|
@@ -6275,9 +6694,20 @@ function apply(ctx, config) {
|
|
|
6275
6694
|
* inheriting a selection that belongs to the other side of the split.
|
|
6276
6695
|
*/
|
|
6277
6696
|
let legacyAccountRegion;
|
|
6278
|
-
|
|
6279
|
-
|
|
6697
|
+
/**
|
|
6698
|
+
* Which account a panel is bound to.
|
|
6699
|
+
*
|
|
6700
|
+
* Keyed by PANEL: the enterprise pool draws on the same CN credentials but
|
|
6701
|
+
* keeps its OWN binding, so pointing the consumer tab at a personal account
|
|
6702
|
+
* never moves the enterprise tab (and vice versa). A panel with no explicit
|
|
6703
|
+
* binding falls through to that REGION's default selection, which is the
|
|
6704
|
+
* store's own "preferred account for this region".
|
|
6705
|
+
*/
|
|
6706
|
+
const effectiveAccountFor = (panel, value) => {
|
|
6707
|
+
const explicit = unwrapVolatile(value.accounts)?.[panel];
|
|
6280
6708
|
if (explicit !== void 0) return explicit;
|
|
6709
|
+
const region = regionOfPanel(panel);
|
|
6710
|
+
if (panel === "enterprise") return void 0;
|
|
6281
6711
|
return legacyAccountRegion === region ? value.accountId : void 0;
|
|
6282
6712
|
};
|
|
6283
6713
|
/**
|
|
@@ -6299,10 +6729,29 @@ function apply(ctx, config) {
|
|
|
6299
6729
|
}
|
|
6300
6730
|
}
|
|
6301
6731
|
}
|
|
6732
|
+
/**
|
|
6733
|
+
* One stack per PANEL.
|
|
6734
|
+
*
|
|
6735
|
+
* `cn` and `enterprise` share a credential REGION but not a directory, a wire
|
|
6736
|
+
* map or a model selection: the two pools publish different rosters, so a
|
|
6737
|
+
* shared catalog would let one pool's answer filter the other's models. Each
|
|
6738
|
+
* panel therefore gets its own catalog, wire state, clients and shim routes,
|
|
6739
|
+
* and the credential store below is the only thing they have in common.
|
|
6740
|
+
*/
|
|
6302
6741
|
const stacks = {};
|
|
6303
|
-
for (const
|
|
6742
|
+
for (const panel of TRAE_PANELS) {
|
|
6743
|
+
const region = regionOfPanel(panel);
|
|
6744
|
+
/**
|
|
6745
|
+
* Whether tenant (enterprise) accounts are also offered by the CONSUMER
|
|
6746
|
+
* panels.
|
|
6747
|
+
*
|
|
6748
|
+
* Off by default, so a published build offers an enterprise login only
|
|
6749
|
+
* under the enterprise panel. Opt-in per machine, and intentionally not
|
|
6750
|
+
* surfaced in user-facing documentation.
|
|
6751
|
+
*/
|
|
6752
|
+
const tenantLocal = process.env["DSH_TRAE_TENANT_IN_CONSUMER"] === "1";
|
|
6304
6753
|
const catalog = new TraeCatalog(region);
|
|
6305
|
-
const wire = wireState[
|
|
6754
|
+
const wire = wireState[panel];
|
|
6306
6755
|
/**
|
|
6307
6756
|
* Best-effort device identity for this region's installation. Prefer the
|
|
6308
6757
|
* selected credential's own edition so an international account reads its
|
|
@@ -6355,6 +6804,7 @@ function apply(ctx, config) {
|
|
|
6355
6804
|
};
|
|
6356
6805
|
const store = new TraeCredentialStore({
|
|
6357
6806
|
region,
|
|
6807
|
+
...panel === "enterprise" ? { requireTenant: true } : { hideTenantAccounts: !tenantLocal },
|
|
6358
6808
|
...unwrapVolatile(config.authFile) === void 0 ? {} : { storagePath: unwrapVolatile(config.authFile) },
|
|
6359
6809
|
edition: unwrapVolatile(config.edition) ?? "auto",
|
|
6360
6810
|
refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
|
|
@@ -6362,19 +6812,65 @@ function apply(ctx, config) {
|
|
|
6362
6812
|
const solo = new TraeSoloUpstreamClient({
|
|
6363
6813
|
credential: () => store.resolve(),
|
|
6364
6814
|
identity,
|
|
6815
|
+
...panel === "enterprise" ? { route: "enterprise" } : {},
|
|
6365
6816
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
6366
6817
|
});
|
|
6367
6818
|
const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
|
|
6368
6819
|
const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
|
|
6369
|
-
|
|
6370
|
-
|
|
6820
|
+
/**
|
|
6821
|
+
* One client + bridge per provider id.
|
|
6822
|
+
*
|
|
6823
|
+
* `routeOfTraeProvider` pins each to its billing pool: the consumer ids get
|
|
6824
|
+
* the region gateway exactly as before, `trae-enterprise` gets the tenant
|
|
6825
|
+
* gateway and stamps the `session_id` it requires. The discovery client
|
|
6826
|
+
* below stays UNROUTED on purpose — it owns the roster/probe path and, for
|
|
6827
|
+
* a tenant credential, reads both pools so the shared catalogue keeps every
|
|
6828
|
+
* model either one serves.
|
|
6829
|
+
*/
|
|
6830
|
+
/**
|
|
6831
|
+
* Latest successful discovery for this panel, mutated by discoverModels
|
|
6832
|
+
* and READ by applySelection (via the stack): a settings write rebuilds
|
|
6833
|
+
* the catalog from saved rows that may predate wireConfigName, and the
|
|
6834
|
+
* merge below is what keeps the case mapping alive across it.
|
|
6835
|
+
*/
|
|
6836
|
+
const latestDiscovery = [];
|
|
6837
|
+
const routeClients = {};
|
|
6838
|
+
const shimRoutes = [];
|
|
6839
|
+
for (const provider of [providerOfPanel(panel)]) {
|
|
6840
|
+
const route = routeOfTraeProvider(provider) ?? "solo";
|
|
6841
|
+
const client = new TraeSoloUpstreamClient({
|
|
6842
|
+
credential: () => store.resolve(),
|
|
6843
|
+
identity,
|
|
6844
|
+
route,
|
|
6845
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
6846
|
+
});
|
|
6847
|
+
const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver));
|
|
6848
|
+
routeClients[provider] = {
|
|
6849
|
+
provider,
|
|
6850
|
+
route,
|
|
6851
|
+
client,
|
|
6852
|
+
upstream: delegating,
|
|
6853
|
+
secret: ""
|
|
6854
|
+
};
|
|
6855
|
+
shimRoutes.push({
|
|
6856
|
+
label: route,
|
|
6857
|
+
client: delegating
|
|
6858
|
+
});
|
|
6859
|
+
}
|
|
6371
6860
|
const shim = createTraeShim({
|
|
6372
6861
|
catalog,
|
|
6373
|
-
|
|
6862
|
+
routes: shimRoutes,
|
|
6374
6863
|
logger: ctx.logger
|
|
6375
6864
|
});
|
|
6865
|
+
for (const entry of Object.values(routeClients)) entry.secret = shim.tokenOf(entry.route);
|
|
6866
|
+
/**
|
|
6867
|
+
* The region's PRIMARY provider plumbing, used by region-wide paths that
|
|
6868
|
+
* are not pool-specific: model probing, Raw-Chat status, and the stack's
|
|
6869
|
+
* `delegating` handle. Per-provider turns never go through this.
|
|
6870
|
+
*/
|
|
6871
|
+
const primary = routeClients[providerOfPanel(panel)];
|
|
6376
6872
|
let rawDiagnostic = () => ({ state: "disabled" });
|
|
6377
|
-
if (
|
|
6873
|
+
if (panel === "cn") (async () => {
|
|
6378
6874
|
try {
|
|
6379
6875
|
const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
|
|
6380
6876
|
const gateway = createTraeRawGateway({
|
|
@@ -6388,7 +6884,7 @@ function apply(ctx, config) {
|
|
|
6388
6884
|
runtime: resolved.runtime
|
|
6389
6885
|
}
|
|
6390
6886
|
}),
|
|
6391
|
-
solo:
|
|
6887
|
+
solo: new TraeSoloBridge(primary.client, catalog, wireResolver),
|
|
6392
6888
|
endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
|
|
6393
6889
|
edition: resolved.identity.edition,
|
|
6394
6890
|
identity: {
|
|
@@ -6398,7 +6894,7 @@ function apply(ctx, config) {
|
|
|
6398
6894
|
runtime: resolved.runtime,
|
|
6399
6895
|
enabled: false
|
|
6400
6896
|
});
|
|
6401
|
-
|
|
6897
|
+
primary.upstream.replace(gateway.upstream);
|
|
6402
6898
|
rawDiagnostic = () => gateway.diagnostic();
|
|
6403
6899
|
ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
|
|
6404
6900
|
} catch (error) {
|
|
@@ -6409,12 +6905,14 @@ function apply(ctx, config) {
|
|
|
6409
6905
|
credential: () => store.resolve(),
|
|
6410
6906
|
deviceId: async () => (await identity()).deviceId
|
|
6411
6907
|
});
|
|
6412
|
-
stacks[
|
|
6908
|
+
stacks[panel] = {
|
|
6413
6909
|
region,
|
|
6414
6910
|
store,
|
|
6415
6911
|
catalog,
|
|
6416
6912
|
shim,
|
|
6417
|
-
|
|
6913
|
+
lastDiscovery: latestDiscovery,
|
|
6914
|
+
delegating: routeClients[providerOfPanel(panel)]?.upstream,
|
|
6915
|
+
routeClients,
|
|
6418
6916
|
usageClient,
|
|
6419
6917
|
/**
|
|
6420
6918
|
* Credit balance of one named account, WITHOUT rebinding the store
|
|
@@ -6483,6 +6981,8 @@ function apply(ctx, config) {
|
|
|
6483
6981
|
wire.byId.set(model.id, target);
|
|
6484
6982
|
wire.byName.set(model.name.trim().toLowerCase(), target);
|
|
6485
6983
|
}
|
|
6984
|
+
latestDiscovery.length = 0;
|
|
6985
|
+
latestDiscovery.push(...merged);
|
|
6486
6986
|
return merged;
|
|
6487
6987
|
},
|
|
6488
6988
|
/**
|
|
@@ -6505,7 +7005,7 @@ function apply(ctx, config) {
|
|
|
6505
7005
|
const skipped = [];
|
|
6506
7006
|
for (const id of ids) if (known.has(id.trim().toLowerCase())) testable.push(id);
|
|
6507
7007
|
else skipped.push(id);
|
|
6508
|
-
const results = await probeModelsSequentially((body, signal) => upstream.chatStream(body, signal), testable);
|
|
7008
|
+
const results = await probeModelsSequentially((body, signal) => primary.upstream.chatStream(body, signal), testable);
|
|
6509
7009
|
return skipped.length === 0 ? { results } : {
|
|
6510
7010
|
results,
|
|
6511
7011
|
skipped
|
|
@@ -6521,9 +7021,10 @@ function apply(ctx, config) {
|
|
|
6521
7021
|
* never moves from here — the card's hint and the follow switch below are
|
|
6522
7022
|
* the only writers.
|
|
6523
7023
|
*/
|
|
6524
|
-
const liveLoginOf = async (
|
|
7024
|
+
const liveLoginOf = async (panel) => {
|
|
7025
|
+
const region = regionOfPanel(panel);
|
|
6525
7026
|
try {
|
|
6526
|
-
const live = await stacks[
|
|
7027
|
+
const live = await stacks[panel].store.liveLogin();
|
|
6527
7028
|
if (live === void 0 || live.region !== region) return void 0;
|
|
6528
7029
|
return {
|
|
6529
7030
|
id: live.id,
|
|
@@ -6547,30 +7048,30 @@ function apply(ctx, config) {
|
|
|
6547
7048
|
* event (`loader/volatile-update`) is what re-runs `applySelection` with the
|
|
6548
7049
|
* updated document — the same channel every card write already uses.
|
|
6549
7050
|
*/
|
|
6550
|
-
const followAppLogin = async (
|
|
6551
|
-
const live = await liveLoginOf(
|
|
7051
|
+
const followAppLogin = async (panel) => {
|
|
7052
|
+
const live = await liveLoginOf(panel);
|
|
6552
7053
|
if (live === void 0 || writeSettings === void 0) return void 0;
|
|
6553
7054
|
const configured = configuredAccountsOf(current());
|
|
6554
|
-
if (configured[
|
|
7055
|
+
if (configured[panel] === live.id) return live;
|
|
6555
7056
|
await writeSettings({ accounts: {
|
|
6556
7057
|
...configured,
|
|
6557
|
-
[
|
|
7058
|
+
[panel]: live.id
|
|
6558
7059
|
} });
|
|
6559
7060
|
return live;
|
|
6560
7061
|
};
|
|
6561
7062
|
ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
|
|
6562
|
-
store: (
|
|
6563
|
-
client: (
|
|
6564
|
-
displayModels: (
|
|
6565
|
-
enabledModelIds: (
|
|
6566
|
-
regionEnabled: (
|
|
6567
|
-
discoverModels: (
|
|
6568
|
-
probeModels: (
|
|
6569
|
-
rawDiagnostic: (
|
|
6570
|
-
creditsOfAccount: (
|
|
6571
|
-
accountStateOf: (
|
|
6572
|
-
claimCheckinOf: (
|
|
6573
|
-
liveLogin: (
|
|
7063
|
+
store: (panel) => stacks[panel].store,
|
|
7064
|
+
client: (panel) => stacks[panel].usageClient,
|
|
7065
|
+
displayModels: (panel) => displayModels(current(), panel),
|
|
7066
|
+
enabledModelIds: (panel) => regionStateOf(current(), panel).enabledModelIds ?? [],
|
|
7067
|
+
regionEnabled: (panel) => regionEnabled(current(), panel),
|
|
7068
|
+
discoverModels: (panel, signal) => stacks[panel].discoverModels(signal),
|
|
7069
|
+
probeModels: (panel, ids) => stacks[panel].probeModels(ids),
|
|
7070
|
+
rawDiagnostic: (panel) => stacks[panel].rawDiagnostic(),
|
|
7071
|
+
creditsOfAccount: (panel, accountId) => stacks[panel].creditsOfAccount(accountId),
|
|
7072
|
+
accountStateOf: (panel, accountId) => stacks[panel].accountStateOf(accountId),
|
|
7073
|
+
claimCheckinOf: (panel, accountId) => stacks[panel].claimCheckinOf(accountId),
|
|
7074
|
+
liveLogin: (panel) => liveLoginOf(panel)
|
|
6574
7075
|
}));
|
|
6575
7076
|
/**
|
|
6576
7077
|
* Live registration handles per region, filled once the shim is listening.
|
|
@@ -6582,8 +7083,18 @@ function apply(ctx, config) {
|
|
|
6582
7083
|
* card tab alone would leave every model selectable.
|
|
6583
7084
|
*/
|
|
6584
7085
|
const registration = {
|
|
6585
|
-
cn: {},
|
|
6586
|
-
|
|
7086
|
+
cn: { adapters: [] },
|
|
7087
|
+
enterprise: { adapters: [] },
|
|
7088
|
+
ai: { adapters: [] }
|
|
7089
|
+
};
|
|
7090
|
+
/** Release every live registration this apply() created. */
|
|
7091
|
+
const disposeRegistration = () => {
|
|
7092
|
+
for (const panel of TRAE_PANELS) {
|
|
7093
|
+
for (const entry of registration[panel].adapters) entry.handle();
|
|
7094
|
+
registration[panel].adapters.length = 0;
|
|
7095
|
+
}
|
|
7096
|
+
registration.cn.directory?.();
|
|
7097
|
+
delete registration.cn.directory;
|
|
6587
7098
|
};
|
|
6588
7099
|
/**
|
|
6589
7100
|
* Publish each region's on/off state to the harness (issue #11). Both swaps
|
|
@@ -6595,21 +7106,53 @@ function apply(ctx, config) {
|
|
|
6595
7106
|
* lands immediately.
|
|
6596
7107
|
*/
|
|
6597
7108
|
const syncRegionRegistration = (value) => {
|
|
6598
|
-
for (const
|
|
6599
|
-
|
|
6600
|
-
provider:
|
|
6601
|
-
|
|
7109
|
+
for (const panel of TRAE_PANELS) {
|
|
7110
|
+
const enabled = regionEnabled(value, panel);
|
|
7111
|
+
for (const entry of registration[panel].adapters) entry.handle.replace(enabled ? [entry.provider] : []);
|
|
7112
|
+
}
|
|
7113
|
+
registration.cn.directory?.replace(TRAE_PANELS.filter((panel) => regionEnabled(value, panel)).map((panel) => ({
|
|
7114
|
+
provider: providerOfPanel(panel),
|
|
7115
|
+
displayName: displayNameOfTraeProvider(providerOfPanel(panel)),
|
|
6602
7116
|
settingsNs,
|
|
6603
7117
|
settingsPath: [],
|
|
6604
7118
|
declared: false
|
|
6605
7119
|
})));
|
|
6606
7120
|
};
|
|
6607
|
-
/** Push the current config into every
|
|
7121
|
+
/** Push the current config into every panel's store selection and catalog. */
|
|
6608
7122
|
const applySelection = (value) => {
|
|
6609
|
-
for (const
|
|
6610
|
-
const stack = stacks[
|
|
6611
|
-
stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(
|
|
6612
|
-
|
|
7123
|
+
for (const panel of TRAE_PANELS) {
|
|
7124
|
+
const stack = stacks[panel];
|
|
7125
|
+
stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(panel, value));
|
|
7126
|
+
/**
|
|
7127
|
+
* Re-merge the wire mapping the last discovery taught us.
|
|
7128
|
+
*
|
|
7129
|
+
* configuredModels rebuilds from the SAVED rows, and rows saved before
|
|
7130
|
+
* wireConfigName existed carry wireFunction alone. Every settings write
|
|
7131
|
+
* goes through here (card writes, follow-app-login, the initial seed),
|
|
7132
|
+
* so without this each write would strip the case mapping until the next
|
|
7133
|
+
* discovery -- the exact window that turns deepseek-v4.1-flash into a
|
|
7134
|
+
* 4001, because the gateway is case-sensitive and the lowercase
|
|
7135
|
+
* directory id is the one spelling it refuses.
|
|
7136
|
+
*/
|
|
7137
|
+
const known = /* @__PURE__ */ new Map();
|
|
7138
|
+
for (const model of stack.lastDiscovery) {
|
|
7139
|
+
known.set(model.id, model);
|
|
7140
|
+
known.set(model.id.trim().toLowerCase(), model);
|
|
7141
|
+
if (model.name.trim() !== "") known.set(model.name.trim().toLowerCase(), model);
|
|
7142
|
+
}
|
|
7143
|
+
const rows = configuredModels(value, panel).map((model) => {
|
|
7144
|
+
const match = known.get(model.id) ?? known.get(model.id.trim().toLowerCase()) ?? (model.name.trim() !== "" ? known.get(model.name.trim().toLowerCase()) : void 0);
|
|
7145
|
+
if (match === void 0) return model;
|
|
7146
|
+
const wireConfigName = model.wireConfigName ?? match.wireConfigName;
|
|
7147
|
+
const wireFunction = model.wireFunction ?? match.wireFunction;
|
|
7148
|
+
if (wireConfigName === model.wireConfigName && wireFunction === model.wireFunction) return model;
|
|
7149
|
+
return {
|
|
7150
|
+
...model,
|
|
7151
|
+
...wireConfigName === void 0 ? {} : { wireConfigName },
|
|
7152
|
+
...wireFunction === void 0 ? {} : { wireFunction }
|
|
7153
|
+
};
|
|
7154
|
+
});
|
|
7155
|
+
stack.catalog.set(rows);
|
|
6613
7156
|
stack.invalidateAdapter();
|
|
6614
7157
|
}
|
|
6615
7158
|
syncRegionRegistration(value);
|
|
@@ -6668,9 +7211,9 @@ function apply(ctx, config) {
|
|
|
6668
7211
|
if (followInFlight || !followAppLoginEnabled(current())) return;
|
|
6669
7212
|
followInFlight = true;
|
|
6670
7213
|
try {
|
|
6671
|
-
for (const
|
|
6672
|
-
if (!regionEnabled(current(),
|
|
6673
|
-
await followAppLogin(
|
|
7214
|
+
for (const panel of TRAE_PANELS) {
|
|
7215
|
+
if (!regionEnabled(current(), panel)) continue;
|
|
7216
|
+
await followAppLogin(panel);
|
|
6674
7217
|
}
|
|
6675
7218
|
} catch (error) {
|
|
6676
7219
|
ctx.logger.warn("dsh-connect-trae: follow-app-login rebind skipped", error);
|
|
@@ -6682,77 +7225,78 @@ function apply(ctx, config) {
|
|
|
6682
7225
|
let stopped = false;
|
|
6683
7226
|
ctx.effect(() => () => {
|
|
6684
7227
|
stopped = true;
|
|
6685
|
-
for (const
|
|
7228
|
+
for (const panel of TRAE_PANELS) stacks[panel].shim.close();
|
|
6686
7229
|
});
|
|
6687
|
-
Promise.all(
|
|
7230
|
+
Promise.all(TRAE_PANELS.map((panel) => stacks[panel].shim.ready)).then(async () => {
|
|
6688
7231
|
if (stopped) return;
|
|
6689
7232
|
try {
|
|
6690
7233
|
const piAi = piAiRuntimeInfo();
|
|
6691
7234
|
ctx.logger.info(`dsh-connect-trae: pi-ai ${piAi.version ?? "unknown"} (${piAi.generation} generation)${piAi.inspected ? "" : " — not resolved locally, the host supplies it"}`);
|
|
6692
|
-
for (const
|
|
6693
|
-
const stack = stacks[
|
|
7235
|
+
for (const panel of TRAE_PANELS) {
|
|
7236
|
+
const stack = stacks[panel];
|
|
7237
|
+
const provider = providerOfPanel(panel);
|
|
7238
|
+
const entry = stack.routeClients[provider];
|
|
7239
|
+
if (entry === void 0) continue;
|
|
6694
7240
|
const trae = createTraeAdapter({
|
|
6695
7241
|
shim: stack.shim,
|
|
6696
7242
|
catalog: stack.catalog,
|
|
6697
|
-
provider
|
|
6698
|
-
displayName:
|
|
7243
|
+
provider,
|
|
7244
|
+
displayName: displayNameOfTraeProvider(provider),
|
|
7245
|
+
secret: entry.secret,
|
|
6699
7246
|
resolveAttachments: () => ctx.get("attachments")
|
|
6700
7247
|
});
|
|
6701
7248
|
stack.invalidateAdapter = () => {
|
|
6702
7249
|
trae.invalidate();
|
|
6703
7250
|
};
|
|
6704
|
-
registration[
|
|
7251
|
+
registration[panel].adapters.push({
|
|
7252
|
+
provider,
|
|
7253
|
+
handle: ctx.llm.registerAdapter([provider], trae.adapter)
|
|
7254
|
+
});
|
|
6705
7255
|
}
|
|
6706
7256
|
ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => {
|
|
6707
|
-
const
|
|
6708
|
-
if (
|
|
6709
|
-
if (!regionEnabled(current(),
|
|
7257
|
+
const panel = panelOfProvider(request.provider ?? "");
|
|
7258
|
+
if (panel === void 0) return [];
|
|
7259
|
+
if (!regionEnabled(current(), panel)) return [];
|
|
6710
7260
|
const cancellation = signal ?? request.signal;
|
|
6711
|
-
|
|
7261
|
+
const next = applyImageSelection(await stacks[panel].discoverModels(cancellation), imageSet(current(), panel));
|
|
7262
|
+
const withRate = panel !== "enterprise";
|
|
7263
|
+
return next.map((model) => ({
|
|
6712
7264
|
id: model.id,
|
|
6713
|
-
name: traeModelDisplayName(model),
|
|
7265
|
+
name: traeModelDisplayName(model, withRate),
|
|
6714
7266
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
6715
7267
|
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
6716
7268
|
inputModalities: traeInputModalities(model)
|
|
6717
7269
|
}));
|
|
6718
7270
|
});
|
|
6719
|
-
registration.cn.directory = ctx.llm.registerConfigurableProviders(
|
|
6720
|
-
provider:
|
|
6721
|
-
displayName:
|
|
7271
|
+
registration.cn.directory = ctx.llm.registerConfigurableProviders(TRAE_PANELS.map((panel) => ({
|
|
7272
|
+
provider: providerOfPanel(panel),
|
|
7273
|
+
displayName: displayNameOfTraeProvider(providerOfPanel(panel)),
|
|
6722
7274
|
settingsNs,
|
|
6723
7275
|
settingsPath: [],
|
|
6724
7276
|
declared: false
|
|
6725
7277
|
})));
|
|
6726
7278
|
syncRegionRegistration(current());
|
|
6727
7279
|
} finally {
|
|
6728
|
-
if (registration.cn.
|
|
6729
|
-
registration.cn.adapter?.();
|
|
6730
|
-
registration.ai.adapter?.();
|
|
6731
|
-
registration.cn.directory?.();
|
|
6732
|
-
}
|
|
7280
|
+
if (registration.cn.adapters.length === 0 || registration.ai.adapters.length === 0 || registration.cn.directory === void 0) disposeRegistration();
|
|
6733
7281
|
}
|
|
6734
7282
|
try {
|
|
6735
7283
|
ctx.effect(() => () => {
|
|
6736
|
-
|
|
6737
|
-
registration.ai.adapter?.();
|
|
6738
|
-
registration.cn.directory?.();
|
|
7284
|
+
disposeRegistration();
|
|
6739
7285
|
});
|
|
6740
7286
|
} catch {
|
|
6741
|
-
|
|
6742
|
-
registration.ai.adapter?.();
|
|
6743
|
-
registration.cn.directory?.();
|
|
7287
|
+
disposeRegistration();
|
|
6744
7288
|
}
|
|
6745
|
-
for (const
|
|
6746
|
-
if (!regionEnabled(current(),
|
|
7289
|
+
for (const panel of TRAE_PANELS) {
|
|
7290
|
+
if (!regionEnabled(current(), panel)) continue;
|
|
6747
7291
|
(async () => {
|
|
6748
|
-
const stack = stacks[
|
|
7292
|
+
const stack = stacks[panel];
|
|
6749
7293
|
try {
|
|
6750
7294
|
const models = await stack.discoverModels();
|
|
6751
7295
|
if (stopped) return;
|
|
6752
|
-
stack.catalog.set(derive(current(), models,
|
|
7296
|
+
stack.catalog.set(derive(current(), models, panel));
|
|
6753
7297
|
} catch (error) {
|
|
6754
|
-
ctx.logger.warn(`dsh-connect-trae: live ${
|
|
6755
|
-
stack.catalog.set(configuredModels(current(),
|
|
7298
|
+
ctx.logger.warn(`dsh-connect-trae: live ${panel} model directory unavailable; serving the configured catalog`, error);
|
|
7299
|
+
stack.catalog.set(configuredModels(current(), panel));
|
|
6756
7300
|
}
|
|
6757
7301
|
stack.invalidateAdapter();
|
|
6758
7302
|
})();
|
|
@@ -6762,4 +7306,4 @@ function apply(ctx, config) {
|
|
|
6762
7306
|
});
|
|
6763
7307
|
}
|
|
6764
7308
|
//#endregion
|
|
6765
|
-
export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
|
|
7309
|
+
export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
|