dsh-connect-trae 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/docs/assets/dsh-connect-trae-usage-card.png +0 -0
- package/lib/client.js +92 -54
- package/lib/index.d.ts +97 -58
- package/lib/index.js +283 -501
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -14,6 +14,161 @@ import { createServer } from "node:http";
|
|
|
14
14
|
import { Readable } from "node:stream";
|
|
15
15
|
import { execFile } from "node:child_process";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
|
+
//#region src/catalog.ts
|
|
18
|
+
/** Bootstrap catalog: identity only where current Trae metadata has not been fetched yet. */
|
|
19
|
+
const FALLBACK_TRAE_MODELS = [
|
|
20
|
+
{
|
|
21
|
+
id: "auto",
|
|
22
|
+
name: "Auto"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "DeepSeek-V4-Flash",
|
|
26
|
+
name: "DeepSeek-V4-Flash"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "DeepSeek-V4-Pro",
|
|
30
|
+
name: "DeepSeek-V4-Pro"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "glm-5.2",
|
|
34
|
+
name: "GLM-5.2"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "kimi-k2.6",
|
|
38
|
+
name: "Kimi-K2.6"
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
/** Exact DSH modalities for one catalog entry; absent metadata is text-only. */
|
|
42
|
+
function traeInputModalities(model) {
|
|
43
|
+
return [...model.input ?? ["text"]];
|
|
44
|
+
}
|
|
45
|
+
/** Apply the user's explicit image opt-ins; upstream and saved row hints are ignored. */
|
|
46
|
+
function applyImageSelection(models, selected) {
|
|
47
|
+
return models.map((model) => ({
|
|
48
|
+
...model,
|
|
49
|
+
input: selected.has(model.id) ? ["text", "image"] : ["text"]
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
/** Normalise a display name for cross-source joining. */
|
|
53
|
+
function displayKey(name) {
|
|
54
|
+
return name.trim().toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Merge the two Trae model sources into one authoritative catalog.
|
|
58
|
+
*
|
|
59
|
+
* `remote` (the solo.trae.cn `/models` directory) is the authoritative model
|
|
60
|
+
* skeleton: it supplies the display id, display name, context windows, credit
|
|
61
|
+
* multiplier, reasoning and multimodal flags. `wire` (from `get_detail_param`)
|
|
62
|
+
* supplies the real `llm_utils_chat` `config_name` — the only id the chat
|
|
63
|
+
* endpoint actually accepts. A remote row is only callable when it maps to a
|
|
64
|
+
* wire `config_name`, so a remote row with no wire match is DROPPED (it would
|
|
65
|
+
* otherwise be sent as an invalid `config_name` and rejected with 4001
|
|
66
|
+
* "param is invalid"). Verified 2026-08-30: the Remote directory advertises
|
|
67
|
+
* `Doubao-Seed-Code` and `glm-5.3`, neither of which is a current `config_name`;
|
|
68
|
+
* both fail every request, so they must not be exposed.
|
|
69
|
+
*
|
|
70
|
+
* Joining is two-tier, in priority order:
|
|
71
|
+
* 1. `wire.id` (the `config_name`) equals the remote id — the model's display
|
|
72
|
+
* id is already its wire id (the common case: glm-5.2, DeepSeek-V4-Flash,
|
|
73
|
+
* kimi-k3, …).
|
|
74
|
+
* 2. `wire.name` (the `display_name`) equals the remote display name — for
|
|
75
|
+
* models whose display id differs from the wire id across Trae versions.
|
|
76
|
+
* When the matched wire `config_name` differs from the remote id it is recorded
|
|
77
|
+
* as `wireConfigName`; otherwise it is left undefined (id is already the wire id).
|
|
78
|
+
*/
|
|
79
|
+
function mergeTraeModelSources(remote, wire) {
|
|
80
|
+
const wireByName = /* @__PURE__ */ new Map();
|
|
81
|
+
const wireById = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const model of wire) {
|
|
83
|
+
wireByName.set(displayKey(model.name), model);
|
|
84
|
+
wireById.set(displayKey(model.id), model);
|
|
85
|
+
}
|
|
86
|
+
const result = [];
|
|
87
|
+
for (const model of remote) {
|
|
88
|
+
const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
|
|
89
|
+
if (wireModel === void 0) continue;
|
|
90
|
+
result.push({
|
|
91
|
+
id: model.id,
|
|
92
|
+
name: model.name,
|
|
93
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
94
|
+
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
95
|
+
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
96
|
+
input: ["text"],
|
|
97
|
+
reasoningSupported: model.reasoningSupported,
|
|
98
|
+
...model.reasoning === void 0 ? {} : {
|
|
99
|
+
reasoning: model.reasoning,
|
|
100
|
+
reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
|
|
101
|
+
},
|
|
102
|
+
...wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Apply the saved local budget. Trae advertises two windows per model (dev and
|
|
109
|
+
* Max), so the budget may only switch a model to its own advertised Max value —
|
|
110
|
+
* never to a fabricated number. Everything else keeps the dev window.
|
|
111
|
+
*/
|
|
112
|
+
function applyContextBudgets(catalog, budgets = {}) {
|
|
113
|
+
return catalog.map((model) => ({
|
|
114
|
+
...model,
|
|
115
|
+
...model.maxContextWindow !== void 0 && budgets[model.id] === model.maxContextWindow ? { contextWindow: model.maxContextWindow } : {}
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
/** Convert Trae metadata into text-only model rows; image support is user-owned configuration. */
|
|
119
|
+
function discoveredCatalog(models) {
|
|
120
|
+
const result = [];
|
|
121
|
+
for (const model of models) result.push({
|
|
122
|
+
id: model.id,
|
|
123
|
+
name: model.name,
|
|
124
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
125
|
+
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
126
|
+
input: ["text"],
|
|
127
|
+
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
128
|
+
reasoningSupported: model.reasoningSupported,
|
|
129
|
+
...model.reasoning === void 0 ? {} : {
|
|
130
|
+
reasoning: model.reasoning,
|
|
131
|
+
reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Drop rows saved by older releases that generated `@1m` variant models, so a
|
|
138
|
+
* stale configuration cannot resurrect a variant the runtime no longer builds.
|
|
139
|
+
*/
|
|
140
|
+
function sanitizeCatalog(catalog) {
|
|
141
|
+
return catalog.filter((model) => {
|
|
142
|
+
if (model.id.endsWith("@1m")) return false;
|
|
143
|
+
const legacy = model;
|
|
144
|
+
return legacy.baseModelId === void 0 && legacy.maxContext !== true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Derive the runtime catalog from the last refreshed Trae directory plus the
|
|
149
|
+
* user's explicit selection and context budgets. An empty selection falls back
|
|
150
|
+
* to the whole directory: a plugin that has never been configured must still
|
|
151
|
+
* serve models rather than nothing. This is the single source of truth for
|
|
152
|
+
* what DSH actually exposes, so saving only the selection and budgets is
|
|
153
|
+
* enough to rebuild it after a restart.
|
|
154
|
+
*/
|
|
155
|
+
function deriveCatalog(catalog, enabled, budgets = {}) {
|
|
156
|
+
return applyContextBudgets(enabled.size === 0 ? catalog : catalog.filter((model) => enabled.has(model.id)), budgets);
|
|
157
|
+
}
|
|
158
|
+
var TraeCatalog = class {
|
|
159
|
+
models = FALLBACK_TRAE_MODELS;
|
|
160
|
+
current() {
|
|
161
|
+
return this.models;
|
|
162
|
+
}
|
|
163
|
+
set(models) {
|
|
164
|
+
if (models.length === 0) throw new Error("trae model catalog cannot be empty");
|
|
165
|
+
this.models = models.map((model) => ({
|
|
166
|
+
...model,
|
|
167
|
+
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
17
172
|
//#region src/adapter.ts
|
|
18
173
|
const TRAE_PROVIDER = "trae";
|
|
19
174
|
const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
@@ -47,7 +202,7 @@ function toPiModel(info, baseUrl) {
|
|
|
47
202
|
api: "openai-completions",
|
|
48
203
|
provider: TRAE_PROVIDER,
|
|
49
204
|
baseUrl,
|
|
50
|
-
input:
|
|
205
|
+
input: traeInputModalities(info),
|
|
51
206
|
cost: {
|
|
52
207
|
input: 0,
|
|
53
208
|
output: 0,
|
|
@@ -528,7 +683,6 @@ var TraeCredentialStore = class {
|
|
|
528
683
|
storagePathOverride;
|
|
529
684
|
edition;
|
|
530
685
|
accountId;
|
|
531
|
-
preferIds = [];
|
|
532
686
|
ownPath;
|
|
533
687
|
refresh;
|
|
534
688
|
refreshMarginMs;
|
|
@@ -551,16 +705,6 @@ var TraeCredentialStore = class {
|
|
|
551
705
|
this.accountId = accountId;
|
|
552
706
|
this.inflight = void 0;
|
|
553
707
|
}
|
|
554
|
-
/**
|
|
555
|
-
* Preferred account ordering used when no account is explicitly selected.
|
|
556
|
-
* The connector can only bill the `llm_utils_chat` channel against general
|
|
557
|
-
* credits, so accounts with zero general credits should not become the
|
|
558
|
-
* default or they fail with 4008 / empty responses.
|
|
559
|
-
*/
|
|
560
|
-
setPreferAccountIds(ids) {
|
|
561
|
-
this.preferIds = [...ids];
|
|
562
|
-
this.inflight = void 0;
|
|
563
|
-
}
|
|
564
708
|
candidates() {
|
|
565
709
|
if (this.storagePathOverride !== void 0) return [{
|
|
566
710
|
edition: this.edition === "auto" ? "cn" : this.edition,
|
|
@@ -569,13 +713,13 @@ var TraeCredentialStore = class {
|
|
|
569
713
|
const all = traeStorageCandidates();
|
|
570
714
|
return this.edition === "auto" ? all.filter((candidate) => candidate.edition === "cn" || candidate.edition === "solo") : all.filter((candidate) => candidate.edition === this.edition && (candidate.edition === "cn" || candidate.edition === "solo"));
|
|
571
715
|
}
|
|
572
|
-
/**
|
|
716
|
+
/**
|
|
717
|
+
* Deterministic default when no account is explicitly selected: the first
|
|
718
|
+
* discovered account. This is NOT credit-seeking — it never reorders accounts
|
|
719
|
+
* to find one with general credits. The plugin bills exactly the account the
|
|
720
|
+
* user selected, or the first account when nothing has been selected yet.
|
|
721
|
+
*/
|
|
573
722
|
preferred(credentials) {
|
|
574
|
-
if (credentials.length === 0) return void 0;
|
|
575
|
-
for (const id of this.preferIds) {
|
|
576
|
-
const match = credentials.find((credential) => traeAccountId(credential) === id);
|
|
577
|
-
if (match !== void 0) return match;
|
|
578
|
-
}
|
|
579
723
|
return credentials[0];
|
|
580
724
|
}
|
|
581
725
|
async accounts() {
|
|
@@ -594,7 +738,7 @@ var TraeCredentialStore = class {
|
|
|
594
738
|
async current() {
|
|
595
739
|
const credentials = await this.readAll();
|
|
596
740
|
if (this.accountId === void 0) return this.preferred(credentials);
|
|
597
|
-
return credentials.find((credential) => traeAccountId(credential) === this.accountId)
|
|
741
|
+
return credentials.find((credential) => traeAccountId(credential) === this.accountId);
|
|
598
742
|
}
|
|
599
743
|
async resolve() {
|
|
600
744
|
const credential = await this.current();
|
|
@@ -685,104 +829,6 @@ var TraeCredentialStore = class {
|
|
|
685
829
|
}
|
|
686
830
|
};
|
|
687
831
|
//#endregion
|
|
688
|
-
//#region src/catalog.ts
|
|
689
|
-
/** Bootstrap catalog: identity only where current Trae metadata has not been fetched yet. */
|
|
690
|
-
const FALLBACK_TRAE_MODELS = [
|
|
691
|
-
{
|
|
692
|
-
id: "auto",
|
|
693
|
-
name: "Auto"
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
id: "DeepSeek-V4-Flash",
|
|
697
|
-
name: "DeepSeek-V4-Flash"
|
|
698
|
-
},
|
|
699
|
-
{
|
|
700
|
-
id: "DeepSeek-V4-Pro",
|
|
701
|
-
name: "DeepSeek-V4-Pro"
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
id: "Doubao-Seed-Code",
|
|
705
|
-
name: "Doubao-Seed-Code",
|
|
706
|
-
input: ["text", "image"]
|
|
707
|
-
},
|
|
708
|
-
{
|
|
709
|
-
id: "kimi-k2.6",
|
|
710
|
-
name: "Kimi-K2.6",
|
|
711
|
-
input: ["text", "image"]
|
|
712
|
-
}
|
|
713
|
-
];
|
|
714
|
-
/** Convert verified Trae API metadata into normal models and optional 1M variants. */
|
|
715
|
-
function discoveredCatalog(models, enabled1m = /* @__PURE__ */ new Set()) {
|
|
716
|
-
const result = [];
|
|
717
|
-
for (const model of models) {
|
|
718
|
-
const common = {
|
|
719
|
-
input: model.multimodal ? ["text", "image"] : ["text"],
|
|
720
|
-
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
721
|
-
reasoningSupported: model.reasoningSupported,
|
|
722
|
-
...model.reasoning === void 0 ? {} : {
|
|
723
|
-
reasoning: model.reasoning,
|
|
724
|
-
reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
result.push({
|
|
728
|
-
id: model.id,
|
|
729
|
-
name: model.name,
|
|
730
|
-
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
731
|
-
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
732
|
-
...common
|
|
733
|
-
});
|
|
734
|
-
if (model.maxContextWindow !== void 0 && enabled1m.has(model.id)) result.push({
|
|
735
|
-
id: `${model.id}@1m`,
|
|
736
|
-
name: `${model.name} 1M`,
|
|
737
|
-
contextWindow: model.maxContextWindow,
|
|
738
|
-
baseModelId: model.id,
|
|
739
|
-
maxContext: true,
|
|
740
|
-
...common
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
return result;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Derive the runtime catalog from the last refreshed Trae directory plus the
|
|
747
|
-
* user's explicit selection. Ordinary models whose `id` (Trae `name`) is
|
|
748
|
-
* enabled survive; each 1M-capable enabled model produces a `@1m` variant.
|
|
749
|
-
* This is the single source of truth for what DSH actually exposes, so saving
|
|
750
|
-
* only the selection is enough to rebuild it after a restart.
|
|
751
|
-
*/
|
|
752
|
-
function deriveCatalog(catalog, enabled, enabled1m) {
|
|
753
|
-
const result = [];
|
|
754
|
-
for (const model of catalog) {
|
|
755
|
-
if (!enabled.has(model.id)) continue;
|
|
756
|
-
result.push(model);
|
|
757
|
-
if (enabled1m.has(model.id) && model.maxContextWindow !== void 0) result.push({
|
|
758
|
-
id: `${model.id}@1m`,
|
|
759
|
-
name: `${model.name} 1M`,
|
|
760
|
-
contextWindow: model.maxContextWindow,
|
|
761
|
-
baseModelId: model.id,
|
|
762
|
-
maxContext: true,
|
|
763
|
-
...model.input === void 0 ? {} : { input: [...model.input] },
|
|
764
|
-
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
765
|
-
...model.reasoningSupported === void 0 ? {} : { reasoningSupported: model.reasoningSupported },
|
|
766
|
-
...model.reasoning === void 0 ? {} : { reasoning: model.reasoning },
|
|
767
|
-
...model.reasoningEfforts === void 0 ? {} : { reasoningEfforts: model.reasoningEfforts }
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
return result;
|
|
771
|
-
}
|
|
772
|
-
var TraeCatalog = class {
|
|
773
|
-
models = FALLBACK_TRAE_MODELS;
|
|
774
|
-
current() {
|
|
775
|
-
return this.models;
|
|
776
|
-
}
|
|
777
|
-
set(models) {
|
|
778
|
-
if (models.length === 0) throw new Error("trae model catalog cannot be empty");
|
|
779
|
-
this.models = models.map((model) => ({
|
|
780
|
-
...model,
|
|
781
|
-
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
782
|
-
}));
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
//#endregion
|
|
786
832
|
//#region src/refresh.ts
|
|
787
833
|
const CLIENT_ID_BY_EDITION = {
|
|
788
834
|
cn: "ono9krqynydwx5",
|
|
@@ -1200,7 +1246,7 @@ function applyReasoningEffort(body, effort, capability) {
|
|
|
1200
1246
|
const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
1201
1247
|
const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
|
|
1202
1248
|
const TRAE_SOLO_MODELS_PATH = "/api/ide/v1/get_detail_param";
|
|
1203
|
-
function classify
|
|
1249
|
+
function classify(status) {
|
|
1204
1250
|
if (status === 401 || status === 403) return "authentication";
|
|
1205
1251
|
if (status === 402) return "hard_credit";
|
|
1206
1252
|
if (status === 429) return "soft_rate";
|
|
@@ -1208,13 +1254,21 @@ function classify$1(status) {
|
|
|
1208
1254
|
if (status >= 500) return "server";
|
|
1209
1255
|
return "client";
|
|
1210
1256
|
}
|
|
1257
|
+
function finitePositive$1(value) {
|
|
1258
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1259
|
+
}
|
|
1211
1260
|
function prepareSoloBody(source, defaultModel = "glm-5.2") {
|
|
1212
|
-
const
|
|
1213
|
-
const model = typeof
|
|
1214
|
-
body
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1261
|
+
const input = JSON.parse(source);
|
|
1262
|
+
const model = typeof input["model"] === "string" && input["model"].trim() !== "" ? input["model"].trim() : defaultModel;
|
|
1263
|
+
const body = {
|
|
1264
|
+
...Array.isArray(input["messages"]) ? { messages: input["messages"] } : {},
|
|
1265
|
+
model,
|
|
1266
|
+
config_name: model,
|
|
1267
|
+
function: TRAE_SOLO_FUNCTION,
|
|
1268
|
+
stream: true,
|
|
1269
|
+
...Array.isArray(input["tools"]) ? { tools: input["tools"] } : {},
|
|
1270
|
+
...typeof input["reasoning_effort"] === "string" ? { reasoning_effort: input["reasoning_effort"] } : {}
|
|
1271
|
+
};
|
|
1218
1272
|
if (Array.isArray(body["messages"])) for (const raw of body["messages"]) {
|
|
1219
1273
|
if (typeof raw !== "object" || raw === null) continue;
|
|
1220
1274
|
const message = raw;
|
|
@@ -1283,8 +1337,11 @@ var TraeSoloUpstreamClient = class {
|
|
|
1283
1337
|
const display = typeof config["display_config"] === "object" && config["display_config"] !== null ? config["display_config"] : {};
|
|
1284
1338
|
const details = Array.isArray(config["model_detail_list"]) ? config["model_detail_list"] : [];
|
|
1285
1339
|
const detail = typeof details[0] === "object" && details[0] !== null ? details[0] : {};
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1340
|
+
const contextTokens = typeof config["context_window_tokens"] === "object" && config["context_window_tokens"] !== null ? config["context_window_tokens"] : {};
|
|
1341
|
+
const promptMaxTokens = finitePositive$1(detail["prompt_max_tokens"]);
|
|
1342
|
+
const devTokens = finitePositive$1(contextTokens["dev"]);
|
|
1343
|
+
const contextWindow = promptMaxTokens ?? devTokens;
|
|
1344
|
+
const maxTokens = finitePositive$1(detail["max_tokens"]);
|
|
1288
1345
|
const reasoning = parseReasoningCapability({
|
|
1289
1346
|
...config,
|
|
1290
1347
|
...detail
|
|
@@ -1335,10 +1392,17 @@ var TraeSoloUpstreamClient = class {
|
|
|
1335
1392
|
response
|
|
1336
1393
|
};
|
|
1337
1394
|
const text = (await response.text()).slice(0, 1024);
|
|
1395
|
+
this.options.log?.("dsh-connect-trae: llm_utils_chat rejected", {
|
|
1396
|
+
status: response.status,
|
|
1397
|
+
model: JSON.parse(prepared)["model"],
|
|
1398
|
+
configName: JSON.parse(prepared)["config_name"],
|
|
1399
|
+
reasoningEffort: JSON.parse(prepared)["reasoning_effort"],
|
|
1400
|
+
body: text
|
|
1401
|
+
});
|
|
1338
1402
|
return {
|
|
1339
1403
|
ok: false,
|
|
1340
1404
|
status: response.status,
|
|
1341
|
-
kind: classify
|
|
1405
|
+
kind: classify(response.status),
|
|
1342
1406
|
message: text || `Trae SOLO returned HTTP ${response.status}`
|
|
1343
1407
|
};
|
|
1344
1408
|
}
|
|
@@ -1485,7 +1549,8 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
1485
1549
|
const encoder = new TextEncoder();
|
|
1486
1550
|
const sse = new SseDecoder();
|
|
1487
1551
|
let sawToolCalls = false;
|
|
1488
|
-
let
|
|
1552
|
+
let emittedFinishReason = false;
|
|
1553
|
+
let upstreamEnded = false;
|
|
1489
1554
|
let upstreamError;
|
|
1490
1555
|
let usage;
|
|
1491
1556
|
const chunk = (delta, finishReason = null) => encoder.encode(`data: ${JSON.stringify({
|
|
@@ -1526,13 +1591,16 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
1526
1591
|
...decoded.outputTokens === void 0 ? {} : { completion_tokens: decoded.outputTokens },
|
|
1527
1592
|
...decoded.totalTokens === void 0 ? {} : { total_tokens: decoded.totalTokens }
|
|
1528
1593
|
};
|
|
1529
|
-
else if (decoded.type === "done"
|
|
1530
|
-
|
|
1531
|
-
if (
|
|
1532
|
-
|
|
1533
|
-
|
|
1594
|
+
else if (decoded.type === "done") {
|
|
1595
|
+
upstreamEnded = true;
|
|
1596
|
+
if (!emittedFinishReason) {
|
|
1597
|
+
emittedFinishReason = true;
|
|
1598
|
+
if (upstreamError !== void 0) {
|
|
1599
|
+
controller.error(upstreamError);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
controller.enqueue(chunk({}, sawToolCalls ? "tool_calls" : decoded.finishReason || "stop"));
|
|
1534
1603
|
}
|
|
1535
|
-
controller.enqueue(chunk({}, sawToolCalls ? "tool_calls" : decoded.finishReason));
|
|
1536
1604
|
}
|
|
1537
1605
|
};
|
|
1538
1606
|
try {
|
|
@@ -1542,11 +1610,14 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
1542
1610
|
for (const event of sse.push(decoder.decode(next.value, { stream: true }))) consume(event);
|
|
1543
1611
|
}
|
|
1544
1612
|
for (const event of sse.finish()) consume(event);
|
|
1545
|
-
if (upstreamError !== void 0 && !
|
|
1613
|
+
if (upstreamError !== void 0 && !upstreamEnded) {
|
|
1546
1614
|
controller.error(upstreamError);
|
|
1547
1615
|
return;
|
|
1548
1616
|
}
|
|
1549
|
-
if (!
|
|
1617
|
+
if (!emittedFinishReason) {
|
|
1618
|
+
emittedFinishReason = true;
|
|
1619
|
+
controller.enqueue(chunk({}, sawToolCalls ? "tool_calls" : "stop"));
|
|
1620
|
+
}
|
|
1550
1621
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1551
1622
|
controller.close();
|
|
1552
1623
|
} catch (error) {
|
|
@@ -1567,14 +1638,34 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
1567
1638
|
/** Native SOLO client wrapper used by the loopback OpenAI adapter. */
|
|
1568
1639
|
var TraeSoloBridge = class {
|
|
1569
1640
|
upstream;
|
|
1570
|
-
|
|
1641
|
+
catalog;
|
|
1642
|
+
wireResolver;
|
|
1643
|
+
constructor(upstream, catalog, wireResolver) {
|
|
1571
1644
|
this.upstream = upstream;
|
|
1645
|
+
this.catalog = catalog;
|
|
1646
|
+
this.wireResolver = wireResolver;
|
|
1572
1647
|
}
|
|
1573
1648
|
async chatStream(bodyJson, signal) {
|
|
1574
1649
|
let model = "glm-5.2";
|
|
1650
|
+
let prepared = bodyJson;
|
|
1575
1651
|
try {
|
|
1576
1652
|
const input = JSON.parse(bodyJson);
|
|
1577
|
-
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"]
|
|
1653
|
+
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
|
|
1654
|
+
const entry = this.catalog?.current().find((item) => item.id === model);
|
|
1655
|
+
const wireModel = entry?.wireConfigName ?? this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "") ?? model;
|
|
1656
|
+
if (wireModel !== input["model"]) {
|
|
1657
|
+
input["model"] = wireModel;
|
|
1658
|
+
prepared = JSON.stringify(input);
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof input["reasoning_effort"] === "string") {
|
|
1661
|
+
const efforts = entry?.reasoningEfforts;
|
|
1662
|
+
const requested = input["reasoning_effort"];
|
|
1663
|
+
const mapped = efforts?.[requested];
|
|
1664
|
+
const allowed = efforts === void 0 ? [] : Object.values(efforts).filter((value) => typeof value === "string");
|
|
1665
|
+
if (typeof mapped === "string") input["reasoning_effort"] = mapped;
|
|
1666
|
+
else if (!allowed.includes(requested)) delete input["reasoning_effort"];
|
|
1667
|
+
prepared = JSON.stringify(input);
|
|
1668
|
+
}
|
|
1578
1669
|
} catch {
|
|
1579
1670
|
return {
|
|
1580
1671
|
ok: false,
|
|
@@ -1583,7 +1674,7 @@ var TraeSoloBridge = class {
|
|
|
1583
1674
|
message: "invalid JSON request"
|
|
1584
1675
|
};
|
|
1585
1676
|
}
|
|
1586
|
-
const result = await this.upstream.chatStream(
|
|
1677
|
+
const result = await this.upstream.chatStream(prepared, signal);
|
|
1587
1678
|
if (!result.ok) return result;
|
|
1588
1679
|
return {
|
|
1589
1680
|
ok: true,
|
|
@@ -1650,147 +1741,20 @@ function parseTraeRemoteModel(value) {
|
|
|
1650
1741
|
//#endregion
|
|
1651
1742
|
//#region src/solo-remote.ts
|
|
1652
1743
|
const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
|
|
1653
|
-
const TRAE_SOLO_REMOTE_MODELS = [
|
|
1654
|
-
{
|
|
1655
|
-
name: "DeepSeek-V4-Flash",
|
|
1656
|
-
displayName: "DeepSeek-V4-Flash",
|
|
1657
|
-
multimodal: false
|
|
1658
|
-
},
|
|
1659
|
-
{
|
|
1660
|
-
name: "DeepSeek-V4-Pro",
|
|
1661
|
-
displayName: "DeepSeek-V4-Pro",
|
|
1662
|
-
multimodal: false
|
|
1663
|
-
},
|
|
1664
|
-
{
|
|
1665
|
-
name: "Doubao_1_6",
|
|
1666
|
-
displayName: "Doubao-Seed-Code",
|
|
1667
|
-
multimodal: true
|
|
1668
|
-
},
|
|
1669
|
-
{
|
|
1670
|
-
name: "Doubao-Seed-2.0-Code",
|
|
1671
|
-
displayName: "Doubao-Seed-2.0-Code",
|
|
1672
|
-
multimodal: true
|
|
1673
|
-
},
|
|
1674
|
-
{
|
|
1675
|
-
name: "kimi-k2.6",
|
|
1676
|
-
displayName: "Kimi-K2.6",
|
|
1677
|
-
multimodal: true
|
|
1678
|
-
},
|
|
1679
|
-
{
|
|
1680
|
-
name: "kimi-k2.5",
|
|
1681
|
-
displayName: "Kimi-K2.5",
|
|
1682
|
-
multimodal: true
|
|
1683
|
-
},
|
|
1684
|
-
{
|
|
1685
|
-
name: "qwen-3.6-plus",
|
|
1686
|
-
displayName: "Qwen3.6-Plus",
|
|
1687
|
-
multimodal: true
|
|
1688
|
-
},
|
|
1689
|
-
{
|
|
1690
|
-
name: "qwen-3.5",
|
|
1691
|
-
displayName: "Qwen3.5",
|
|
1692
|
-
multimodal: true
|
|
1693
|
-
},
|
|
1694
|
-
{
|
|
1695
|
-
name: "glm-5.1",
|
|
1696
|
-
displayName: "GLM-5.1",
|
|
1697
|
-
multimodal: false
|
|
1698
|
-
},
|
|
1699
|
-
{
|
|
1700
|
-
name: "glm-5",
|
|
1701
|
-
displayName: "GLM-5",
|
|
1702
|
-
multimodal: false
|
|
1703
|
-
},
|
|
1704
|
-
{
|
|
1705
|
-
name: "glm-5v-turbo",
|
|
1706
|
-
displayName: "GLM-5V-Turbo",
|
|
1707
|
-
multimodal: true
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
name: "minimax-m2.7",
|
|
1711
|
-
displayName: "MiniMax-M2.7",
|
|
1712
|
-
multimodal: false
|
|
1713
|
-
},
|
|
1714
|
-
{
|
|
1715
|
-
name: "minimax-m2.5",
|
|
1716
|
-
displayName: "MiniMax-M2.5",
|
|
1717
|
-
multimodal: false
|
|
1718
|
-
}
|
|
1719
|
-
];
|
|
1720
1744
|
/**
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1745
|
+
* Read-only model catalog client for the SOLO Web API.
|
|
1746
|
+
*
|
|
1747
|
+
* This deliberately has no chat/session method: the Remote session protocol
|
|
1748
|
+
* only exposes a final answer and cannot preserve DSH's structured tool loop.
|
|
1725
1749
|
*/
|
|
1726
|
-
|
|
1727
|
-
if (messages.length === 1 && messages[0]?.role === "user") return messages[0].content;
|
|
1728
|
-
return messages.map((message) => `<dsh-message role="${message.role}">\n${message.content}\n</dsh-message>`).join("\n\n");
|
|
1729
|
-
}
|
|
1730
|
-
function decodeUserId(token) {
|
|
1731
|
-
try {
|
|
1732
|
-
return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64").toString("utf8")).data?.id ?? "0";
|
|
1733
|
-
} catch {
|
|
1734
|
-
return "0";
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
function generateWebId() {
|
|
1738
|
-
return String(Math.floor(Math.random() * 9e15) + 0x38d7ea4c68000);
|
|
1739
|
-
}
|
|
1740
|
-
function buildCommonParams(userId, webId, sessionId) {
|
|
1741
|
-
return JSON.stringify({
|
|
1742
|
-
language: "zh-cn",
|
|
1743
|
-
app_language: "en",
|
|
1744
|
-
quality: "stable",
|
|
1745
|
-
app_version: "1.0.0.1300",
|
|
1746
|
-
user_identity: "Free",
|
|
1747
|
-
is_freshman: "0",
|
|
1748
|
-
scope: "marscode",
|
|
1749
|
-
tenant: "marscode",
|
|
1750
|
-
region: "CN",
|
|
1751
|
-
aiRegion: "CN",
|
|
1752
|
-
solo_chat_mode: "code",
|
|
1753
|
-
is_privacy_mode: 1,
|
|
1754
|
-
privacy_mode: "on",
|
|
1755
|
-
...sessionId === void 0 ? {} : { biz_session_id: sessionId },
|
|
1756
|
-
web_id: webId,
|
|
1757
|
-
biz_user_id: userId,
|
|
1758
|
-
user_unique_id: userId
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
function extractContent(raw) {
|
|
1762
|
-
try {
|
|
1763
|
-
const parsed = JSON.parse(raw);
|
|
1764
|
-
if (Array.isArray(parsed)) return parsed.filter((c) => typeof c === "object" && c !== null && c.type === "text").map((c) => c.text_content ?? "").join("");
|
|
1765
|
-
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.messages)) {
|
|
1766
|
-
const parts = [];
|
|
1767
|
-
for (const msg of parsed.messages) {
|
|
1768
|
-
if (typeof msg !== "object" || msg === null) continue;
|
|
1769
|
-
const pi = msg.plan_item;
|
|
1770
|
-
if (typeof pi !== "object" || pi === null) continue;
|
|
1771
|
-
const tc = pi.tool_call_info;
|
|
1772
|
-
if (typeof tc !== "object" || tc === null) continue;
|
|
1773
|
-
const tool = tc;
|
|
1774
|
-
if (tool.name === "finish" && tool.params?.summary) parts.push(tool.params.summary);
|
|
1775
|
-
else if (tool.result?.data?.summary && tool.name !== "finish") parts.push(tool.result.data.summary);
|
|
1776
|
-
}
|
|
1777
|
-
return parts.join("\n\n");
|
|
1778
|
-
}
|
|
1779
|
-
} catch {}
|
|
1780
|
-
return raw;
|
|
1781
|
-
}
|
|
1782
|
-
var TraeSoloRemoteClient = class {
|
|
1750
|
+
var TraeSoloRemoteCatalogClient = class {
|
|
1783
1751
|
options;
|
|
1784
1752
|
fetchImpl;
|
|
1785
1753
|
baseUrl;
|
|
1786
|
-
pollIntervalMs;
|
|
1787
|
-
timeoutMs;
|
|
1788
1754
|
constructor(options) {
|
|
1789
1755
|
this.options = options;
|
|
1790
1756
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1791
1757
|
this.baseUrl = options.baseUrl ?? "https://solo.trae.cn/api/remote/v1";
|
|
1792
|
-
this.pollIntervalMs = options.pollIntervalMs ?? 2e3;
|
|
1793
|
-
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
1794
1758
|
}
|
|
1795
1759
|
async headers() {
|
|
1796
1760
|
return {
|
|
@@ -1803,77 +1767,6 @@ var TraeSoloRemoteClient = class {
|
|
|
1803
1767
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1804
1768
|
};
|
|
1805
1769
|
}
|
|
1806
|
-
async chat(messages, model, signal) {
|
|
1807
|
-
const headers = await this.headers();
|
|
1808
|
-
const token = (await this.options.credential()).accessToken;
|
|
1809
|
-
const userId = decodeUserId(token);
|
|
1810
|
-
const webId = generateWebId();
|
|
1811
|
-
const modelInfo = TRAE_SOLO_REMOTE_MODELS.find((m) => m.name.toLowerCase() === model.toLowerCase()) ?? {
|
|
1812
|
-
name: model,
|
|
1813
|
-
displayName: model,
|
|
1814
|
-
multimodal: false
|
|
1815
|
-
};
|
|
1816
|
-
const conversation = serializeTraeConversation(messages);
|
|
1817
|
-
const body = {
|
|
1818
|
-
mode: "code",
|
|
1819
|
-
environment_id: "default",
|
|
1820
|
-
env: "remote",
|
|
1821
|
-
auto_create_project: false,
|
|
1822
|
-
origin: "web",
|
|
1823
|
-
initial_message: {
|
|
1824
|
-
chat_session_id: "",
|
|
1825
|
-
content: [],
|
|
1826
|
-
query: JSON.stringify([{
|
|
1827
|
-
type: "text",
|
|
1828
|
-
data: { content: conversation }
|
|
1829
|
-
}]),
|
|
1830
|
-
model_name: modelInfo.name,
|
|
1831
|
-
agent_type: "solo_agent_remote",
|
|
1832
|
-
model_selection_strategy: "manual",
|
|
1833
|
-
custom_model: {
|
|
1834
|
-
name: modelInfo.name,
|
|
1835
|
-
multimodal: modelInfo.multimodal,
|
|
1836
|
-
is_default: false,
|
|
1837
|
-
display_name: modelInfo.displayName,
|
|
1838
|
-
config_name: modelInfo.name,
|
|
1839
|
-
config_source: 1,
|
|
1840
|
-
provider: "",
|
|
1841
|
-
ak: "",
|
|
1842
|
-
sk: "",
|
|
1843
|
-
base_url: "",
|
|
1844
|
-
auth_type: 0,
|
|
1845
|
-
use_remote_service: true
|
|
1846
|
-
},
|
|
1847
|
-
common_params: buildCommonParams(userId, webId)
|
|
1848
|
-
}
|
|
1849
|
-
};
|
|
1850
|
-
const createResponse = await this.fetchImpl(`${this.baseUrl}/chat_sessions`, {
|
|
1851
|
-
method: "POST",
|
|
1852
|
-
headers,
|
|
1853
|
-
body: JSON.stringify(body),
|
|
1854
|
-
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1855
|
-
});
|
|
1856
|
-
const createJson = await createResponse.json();
|
|
1857
|
-
if (!createResponse.ok || createJson.code !== 0 || !createJson.data?.chat_session_id) throw new Error(`SOLO remote session creation failed (http ${createResponse.status}): ${createJson.message ?? "unknown error"}`);
|
|
1858
|
-
const sessionId = createJson.data.chat_session_id;
|
|
1859
|
-
const start = Date.now();
|
|
1860
|
-
while (Date.now() - start < this.timeoutMs) {
|
|
1861
|
-
if (signal?.aborted) throw new Error("SOLO remote polling aborted");
|
|
1862
|
-
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1863
|
-
const messageResponse = await this.fetchImpl(`${this.baseUrl}/chat_sessions/${sessionId}/messages?page_size=50`, {
|
|
1864
|
-
headers,
|
|
1865
|
-
...signal === void 0 ? {} : { signal }
|
|
1866
|
-
});
|
|
1867
|
-
if (!messageResponse.ok) continue;
|
|
1868
|
-
const assistant = ((await messageResponse.json()).data?.items ?? []).find((item) => item.role === "assistant");
|
|
1869
|
-
if (assistant && assistant.status !== "in_progress" && assistant.status !== "failed") return {
|
|
1870
|
-
content: extractContent(assistant.content ?? ""),
|
|
1871
|
-
sessionId
|
|
1872
|
-
};
|
|
1873
|
-
if (assistant?.status === "failed") throw new Error("SOLO remote assistant message failed");
|
|
1874
|
-
}
|
|
1875
|
-
throw new Error("SOLO remote polling timed out");
|
|
1876
|
-
}
|
|
1877
1770
|
async fetchModels(signal) {
|
|
1878
1771
|
const headers = await this.headers();
|
|
1879
1772
|
const response = await this.fetchImpl(`${this.baseUrl}/models?functions=solo_agent_remote,solo_work_remote`, {
|
|
@@ -1891,6 +1784,7 @@ var TraeSoloRemoteClient = class {
|
|
|
1891
1784
|
seen.add(model.id);
|
|
1892
1785
|
models.push(model);
|
|
1893
1786
|
}
|
|
1787
|
+
if (models.length === 0) throw new Error("SOLO remote models response contained no models");
|
|
1894
1788
|
return models;
|
|
1895
1789
|
}
|
|
1896
1790
|
};
|
|
@@ -2239,7 +2133,7 @@ function rawCapabilityDiagnostic(enabled, capability, checkedAtMs) {
|
|
|
2239
2133
|
//#endregion
|
|
2240
2134
|
//#region src/fallback-upstream.ts
|
|
2241
2135
|
/**
|
|
2242
|
-
*
|
|
2136
|
+
* Optional primary route first, native SOLO tool-call route second. Fallback is intentionally narrow:
|
|
2243
2137
|
* only definitive pre-stream schema/route incompatibilities may retry the same
|
|
2244
2138
|
* user request. Authentication, credit, rate, cancellation, transport/server
|
|
2245
2139
|
* failures, and every successful Response are never replayed.
|
|
@@ -2606,7 +2500,6 @@ async function traeWebUsage(deps) {
|
|
|
2606
2500
|
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
2607
2501
|
})),
|
|
2608
2502
|
enabledModelIds: [...deps.enabledModelIds()],
|
|
2609
|
-
enabled1mModelIds: [...deps.enabled1mModelIds()],
|
|
2610
2503
|
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic() }
|
|
2611
2504
|
};
|
|
2612
2505
|
try {
|
|
@@ -2797,125 +2690,6 @@ function buildTraeFusionRawChatEnvelope(arg, config) {
|
|
|
2797
2690
|
};
|
|
2798
2691
|
}
|
|
2799
2692
|
//#endregion
|
|
2800
|
-
//#region src/solo-remote-bridge.ts
|
|
2801
|
-
function classify(status) {
|
|
2802
|
-
if (status === 401 || status === 403) return "authentication";
|
|
2803
|
-
if (status === 402) return "hard_credit";
|
|
2804
|
-
if (status === 429) return "soft_rate";
|
|
2805
|
-
if (status === 404) return "not_found";
|
|
2806
|
-
if (status >= 500) return "server";
|
|
2807
|
-
return "client";
|
|
2808
|
-
}
|
|
2809
|
-
/**
|
|
2810
|
-
* Bridges the SOLO remote polling-based API into the shim's stream interface.
|
|
2811
|
-
* The remote API uses create-session + poll-messages rather than raw SSE,
|
|
2812
|
-
* so this bridge converts the final result into a synthetic OpenAI SSE stream.
|
|
2813
|
-
*/
|
|
2814
|
-
var TraeSoloRemoteBridge = class {
|
|
2815
|
-
remote;
|
|
2816
|
-
constructor(remote) {
|
|
2817
|
-
this.remote = remote;
|
|
2818
|
-
}
|
|
2819
|
-
async chatStream(bodyJson, signal) {
|
|
2820
|
-
let parsed;
|
|
2821
|
-
try {
|
|
2822
|
-
parsed = JSON.parse(bodyJson);
|
|
2823
|
-
} catch {
|
|
2824
|
-
return {
|
|
2825
|
-
ok: false,
|
|
2826
|
-
status: 400,
|
|
2827
|
-
kind: "client",
|
|
2828
|
-
message: "invalid JSON request"
|
|
2829
|
-
};
|
|
2830
|
-
}
|
|
2831
|
-
if (!Array.isArray(parsed.messages) || parsed.messages.length === 0) return {
|
|
2832
|
-
ok: false,
|
|
2833
|
-
status: 400,
|
|
2834
|
-
kind: "client",
|
|
2835
|
-
message: "messages are required"
|
|
2836
|
-
};
|
|
2837
|
-
const messages = parsed.messages.flatMap((message) => {
|
|
2838
|
-
if (typeof message.role !== "string") return [];
|
|
2839
|
-
if (typeof message.content === "string") return [{
|
|
2840
|
-
role: message.role,
|
|
2841
|
-
content: message.content
|
|
2842
|
-
}];
|
|
2843
|
-
if (!Array.isArray(message.content)) return [];
|
|
2844
|
-
const content = message.content.flatMap((part) => {
|
|
2845
|
-
if (typeof part !== "object" || part === null) return [];
|
|
2846
|
-
const item = part;
|
|
2847
|
-
const text = typeof item.text === "string" ? item.text : typeof item.content === "string" ? item.content : void 0;
|
|
2848
|
-
return text === void 0 ? [] : [text];
|
|
2849
|
-
}).join("\n");
|
|
2850
|
-
return content === "" ? [] : [{
|
|
2851
|
-
role: message.role,
|
|
2852
|
-
content
|
|
2853
|
-
}];
|
|
2854
|
-
});
|
|
2855
|
-
if (messages.length === 0) return {
|
|
2856
|
-
ok: false,
|
|
2857
|
-
status: 400,
|
|
2858
|
-
kind: "client",
|
|
2859
|
-
message: "text messages are required"
|
|
2860
|
-
};
|
|
2861
|
-
const model = typeof parsed.model === "string" && parsed.model !== "" ? parsed.model : "DeepSeek-V4-Flash";
|
|
2862
|
-
const upstreamModel = model.endsWith("@1m") ? model.slice(0, -3) : model;
|
|
2863
|
-
try {
|
|
2864
|
-
const chunks = buildOpenAISseChunks((await this.remote.chat(messages, upstreamModel, signal)).content, model);
|
|
2865
|
-
const encoder = new TextEncoder();
|
|
2866
|
-
const stream = new ReadableStream({ start(controller) {
|
|
2867
|
-
for (const chunk of chunks) controller.enqueue(encoder.encode(chunk));
|
|
2868
|
-
controller.close();
|
|
2869
|
-
} });
|
|
2870
|
-
return {
|
|
2871
|
-
ok: true,
|
|
2872
|
-
response: new Response(stream, {
|
|
2873
|
-
status: 200,
|
|
2874
|
-
headers: { "Content-Type": "text/event-stream" }
|
|
2875
|
-
})
|
|
2876
|
-
};
|
|
2877
|
-
} catch (error) {
|
|
2878
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2879
|
-
const status = /timed out/i.test(message) ? 504 : /session creation failed/i.test(message) ? 502 : 502;
|
|
2880
|
-
return {
|
|
2881
|
-
ok: false,
|
|
2882
|
-
status,
|
|
2883
|
-
kind: classify(status),
|
|
2884
|
-
message: message.slice(0, 400)
|
|
2885
|
-
};
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
};
|
|
2889
|
-
function buildOpenAISseChunks(content, model) {
|
|
2890
|
-
const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
2891
|
-
const created = Math.floor(Date.now() / 1e3);
|
|
2892
|
-
const chunks = [];
|
|
2893
|
-
if (content !== "") chunks.push(`data: ${JSON.stringify({
|
|
2894
|
-
id,
|
|
2895
|
-
object: "chat.completion.chunk",
|
|
2896
|
-
created,
|
|
2897
|
-
model,
|
|
2898
|
-
choices: [{
|
|
2899
|
-
index: 0,
|
|
2900
|
-
delta: { content },
|
|
2901
|
-
finish_reason: null
|
|
2902
|
-
}]
|
|
2903
|
-
})}\n\n`);
|
|
2904
|
-
chunks.push(`data: ${JSON.stringify({
|
|
2905
|
-
id,
|
|
2906
|
-
object: "chat.completion.chunk",
|
|
2907
|
-
created,
|
|
2908
|
-
model,
|
|
2909
|
-
choices: [{
|
|
2910
|
-
index: 0,
|
|
2911
|
-
delta: {},
|
|
2912
|
-
finish_reason: "stop"
|
|
2913
|
-
}]
|
|
2914
|
-
})}\n\n`);
|
|
2915
|
-
chunks.push("data: [DONE]\n\n");
|
|
2916
|
-
return chunks;
|
|
2917
|
-
}
|
|
2918
|
-
//#endregion
|
|
2919
2693
|
//#region src/upstream.ts
|
|
2920
2694
|
/**
|
|
2921
2695
|
* Safe first-stage upstream. Real Trae traffic stays disabled until one
|
|
@@ -2955,18 +2729,26 @@ const Config = z.object({
|
|
|
2955
2729
|
accountId: z.string().description("Selected local Trae account id (never a token)"),
|
|
2956
2730
|
lastCatalog: z.array(modelConfig).description("Last refreshed Trae raw model directory shown by the plugin card"),
|
|
2957
2731
|
enabledModelIds: z.array(z.string()).default([]).description("Trae model ids the user enabled"),
|
|
2958
|
-
|
|
2732
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Local DSH context budget per Trae model"),
|
|
2733
|
+
imageModelIds: z.array(z.string()).default([]).description("Trae model ids the user explicitly enabled for image input"),
|
|
2959
2734
|
models: z.array(modelConfig).description("Legacy generated Trae model list")
|
|
2960
2735
|
});
|
|
2961
2736
|
function apply(ctx, config) {
|
|
2962
2737
|
const catalog = new TraeCatalog();
|
|
2963
2738
|
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
2964
|
-
const
|
|
2965
|
-
const
|
|
2966
|
-
|
|
2967
|
-
|
|
2739
|
+
const imageSet = (value) => new Set(value.imageModelIds ?? []);
|
|
2740
|
+
const callableKeys = /* @__PURE__ */ new Set();
|
|
2741
|
+
const dropDeadModels = (rows) => {
|
|
2742
|
+
if (callableKeys.size === 0) return rows;
|
|
2743
|
+
return rows.filter((model) => callableKeys.has(model.id.trim().toLowerCase()) || callableKeys.has(model.name.trim().toLowerCase()));
|
|
2744
|
+
};
|
|
2745
|
+
const derive = (value, raw) => {
|
|
2746
|
+
const selectedImages = imageSet(value);
|
|
2747
|
+
const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw)), selectedImages), enabledSet(value), value.contextBudgets ?? {});
|
|
2748
|
+
return derived.length > 0 ? derived : applyImageSelection(dropDeadModels(FALLBACK_TRAE_MODELS), selectedImages);
|
|
2968
2749
|
};
|
|
2969
|
-
const
|
|
2750
|
+
const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) : applyImageSelection(dropDeadModels(FALLBACK_TRAE_MODELS), imageSet(value));
|
|
2751
|
+
const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) : dropDeadModels(FALLBACK_TRAE_MODELS);
|
|
2970
2752
|
const store = new TraeCredentialStore({
|
|
2971
2753
|
...config.authFile === void 0 ? {} : { storagePath: config.authFile },
|
|
2972
2754
|
edition: config.edition ?? "auto",
|
|
@@ -2984,10 +2766,14 @@ function apply(ctx, config) {
|
|
|
2984
2766
|
const solo = new TraeSoloUpstreamClient({
|
|
2985
2767
|
credential: () => store.resolve(),
|
|
2986
2768
|
identity,
|
|
2987
|
-
baseUrl: "https://trae-api-cn.mchost.guru"
|
|
2769
|
+
baseUrl: "https://trae-api-cn.mchost.guru",
|
|
2770
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
2988
2771
|
});
|
|
2989
|
-
const
|
|
2990
|
-
const
|
|
2772
|
+
const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
|
|
2773
|
+
const wireById = /* @__PURE__ */ new Map();
|
|
2774
|
+
const wireByName = /* @__PURE__ */ new Map();
|
|
2775
|
+
const wireResolver = (displayId) => wireById.get(displayId) ?? wireByName.get(displayId.trim().toLowerCase());
|
|
2776
|
+
const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
|
|
2991
2777
|
const delegating = new TraeDelegatingUpstreamClient(upstream);
|
|
2992
2778
|
const shim = createTraeShim({
|
|
2993
2779
|
catalog,
|
|
@@ -3027,41 +2813,32 @@ function apply(ctx, config) {
|
|
|
3027
2813
|
ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
|
|
3028
2814
|
}
|
|
3029
2815
|
})();
|
|
3030
|
-
const selectPreferredGeneralAccount = async () => {
|
|
3031
|
-
try {
|
|
3032
|
-
const accounts = await store.accounts();
|
|
3033
|
-
const scored = [];
|
|
3034
|
-
for (const account of accounts) try {
|
|
3035
|
-
store.selectAccount(account.id);
|
|
3036
|
-
const credential = await store.resolve();
|
|
3037
|
-
const snapshot = await new TraeUsageClient({ credential: async () => credential }).snapshot();
|
|
3038
|
-
const general = Math.round(snapshot.packs.filter((pack) => pack.availableEndpoint === 0).reduce((sum, pack) => sum + Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0)), 0) * 1e4) / 1e4;
|
|
3039
|
-
scored.push({
|
|
3040
|
-
id: account.id,
|
|
3041
|
-
general
|
|
3042
|
-
});
|
|
3043
|
-
} catch (error) {
|
|
3044
|
-
ctx.logger.warn(`dsh-connect-trae: account ${account.id} unavailable for default selection`, error);
|
|
3045
|
-
}
|
|
3046
|
-
scored.sort((a, b) => b.general - a.general);
|
|
3047
|
-
store.setPreferAccountIds(scored.filter((item) => item.general > 0).map((item) => item.id));
|
|
3048
|
-
store.setSource(config.authFile, config.edition ?? "auto", config.accountId);
|
|
3049
|
-
} catch (error) {
|
|
3050
|
-
ctx.logger.warn("dsh-connect-trae: default account selection failed; continuing", error);
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
2816
|
const usageClient = new TraeUsageClient({ credential: () => store.resolve() });
|
|
3054
2817
|
let current = () => config;
|
|
3055
2818
|
let invalidateAdapter = () => {};
|
|
3056
2819
|
const discoverModels = async (signal) => {
|
|
3057
|
-
|
|
2820
|
+
const [remote, wire] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
|
|
2821
|
+
const merged = mergeTraeModelSources(remote, wire);
|
|
2822
|
+
callableKeys.clear();
|
|
2823
|
+
for (const model of merged) {
|
|
2824
|
+
callableKeys.add(model.id.trim().toLowerCase());
|
|
2825
|
+
callableKeys.add(model.name.trim().toLowerCase());
|
|
2826
|
+
}
|
|
2827
|
+
wireById.clear();
|
|
2828
|
+
wireByName.clear();
|
|
2829
|
+
for (const model of merged) if (model.wireConfigName !== void 0) {
|
|
2830
|
+
wireById.set(model.id, model.wireConfigName);
|
|
2831
|
+
wireByName.set(model.name.trim().toLowerCase(), model.wireConfigName);
|
|
2832
|
+
}
|
|
2833
|
+
const next = applyImageSelection(merged, imageSet(current()));
|
|
2834
|
+
catalog.set(next);
|
|
2835
|
+
return merged;
|
|
3058
2836
|
};
|
|
3059
2837
|
ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
|
|
3060
2838
|
store,
|
|
3061
2839
|
client: usageClient,
|
|
3062
2840
|
displayModels: () => displayModels(current()),
|
|
3063
2841
|
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
3064
|
-
enabled1mModelIds: () => current().enabled1mModels ?? [],
|
|
3065
2842
|
discoverModels,
|
|
3066
2843
|
rawDiagnostic: () => rawDiagnostic()
|
|
3067
2844
|
}));
|
|
@@ -3083,7 +2860,11 @@ function apply(ctx, config) {
|
|
|
3083
2860
|
});
|
|
3084
2861
|
shim.ready.then(async () => {
|
|
3085
2862
|
if (stopped) return;
|
|
3086
|
-
|
|
2863
|
+
try {
|
|
2864
|
+
await discoverModels();
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
ctx.logger.warn("dsh-connect-trae: wire-id resolution failed at startup; falling back to display ids", error);
|
|
2867
|
+
}
|
|
3087
2868
|
catalog.set(configuredModels(current()));
|
|
3088
2869
|
const trae = createTraeAdapter({
|
|
3089
2870
|
shim,
|
|
@@ -3099,11 +2880,12 @@ function apply(ctx, config) {
|
|
|
3099
2880
|
releaseAdapter = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
|
|
3100
2881
|
ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request) => {
|
|
3101
2882
|
if (request.provider !== "trae") return [];
|
|
3102
|
-
return (await discoverModels(request.signal)).map((model) => ({
|
|
2883
|
+
return applyImageSelection(await discoverModels(request.signal), imageSet(current())).map((model) => ({
|
|
3103
2884
|
id: model.id,
|
|
3104
2885
|
name: model.name,
|
|
3105
2886
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
3106
|
-
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
|
|
2887
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
2888
|
+
inputModalities: traeInputModalities(model)
|
|
3107
2889
|
}));
|
|
3108
2890
|
});
|
|
3109
2891
|
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
@@ -3133,4 +2915,4 @@ function apply(ctx, config) {
|
|
|
3133
2915
|
});
|
|
3134
2916
|
}
|
|
3135
2917
|
//#endregion
|
|
3136
|
-
export { Config, FALLBACK_TRAE_MODELS, SseDecoder, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE,
|
|
2918
|
+
export { Config, FALLBACK_TRAE_MODELS, SseDecoder, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, hashTraeRawChatArg, identityHeaders, inject, mergeTraeModelSources, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeIdentity, refreshTraeCredential, registerTraeUsageRoute, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage };
|