dsh-connect-trae 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.en.md +76 -0
- package/README.md +95 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/cordis.patch.yml +4 -0
- package/docs/assets/dsh-connect-trae-usage-card.png +0 -0
- package/lib/client.js +752 -0
- package/lib/index.d.ts +975 -0
- package/lib/index.js +3118 -0
- package/package.json +111 -0
- package/screenshots.json +3 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,3118 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
4
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
5
|
+
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
7
|
+
import { readFile, rm, stat } from "node:fs/promises";
|
|
8
|
+
import { createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
11
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
12
|
+
import { cpus, homedir, release } from "node:os";
|
|
13
|
+
import { createServer } from "node:http";
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
//#region src/adapter.ts
|
|
18
|
+
const TRAE_PROVIDER = "trae";
|
|
19
|
+
const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
20
|
+
const INERT_AUTH = {
|
|
21
|
+
credentials: {
|
|
22
|
+
async read() {},
|
|
23
|
+
async list() {
|
|
24
|
+
return [];
|
|
25
|
+
},
|
|
26
|
+
async modify() {
|
|
27
|
+
throw new Error("dsh-connect-trae has no pi-ai credential lifecycle");
|
|
28
|
+
},
|
|
29
|
+
async delete() {}
|
|
30
|
+
},
|
|
31
|
+
authContext: {
|
|
32
|
+
async env() {},
|
|
33
|
+
async fileExists() {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const REQUEST_IMAGE_BUDGETS = {
|
|
39
|
+
maxRequestImageBytes: 20971520,
|
|
40
|
+
requestImagePixelBudget: 4194304,
|
|
41
|
+
requestImageMaxBytes: 1048576
|
|
42
|
+
};
|
|
43
|
+
function toPiModel(info, baseUrl) {
|
|
44
|
+
return {
|
|
45
|
+
id: info.id,
|
|
46
|
+
name: info.name,
|
|
47
|
+
api: "openai-completions",
|
|
48
|
+
provider: TRAE_PROVIDER,
|
|
49
|
+
baseUrl,
|
|
50
|
+
input: [...info.input ?? ["text"]],
|
|
51
|
+
cost: {
|
|
52
|
+
input: 0,
|
|
53
|
+
output: 0,
|
|
54
|
+
cacheRead: 0,
|
|
55
|
+
cacheWrite: 0
|
|
56
|
+
},
|
|
57
|
+
reasoning: info.reasoningEfforts !== void 0,
|
|
58
|
+
...info.reasoningEfforts === void 0 ? {} : { thinkingLevelMap: {
|
|
59
|
+
off: null,
|
|
60
|
+
minimal: null,
|
|
61
|
+
low: info.reasoningEfforts.low ?? null,
|
|
62
|
+
medium: null,
|
|
63
|
+
high: info.reasoningEfforts.high ?? null,
|
|
64
|
+
xhigh: info.reasoningEfforts.xhigh ?? null,
|
|
65
|
+
max: null
|
|
66
|
+
} },
|
|
67
|
+
...info.contextWindow === void 0 ? {} : { contextWindow: info.contextWindow },
|
|
68
|
+
...info.maxTokens === void 0 ? {} : { maxTokens: info.maxTokens },
|
|
69
|
+
compat: { supportsReasoningEffort: info.reasoningEfforts !== void 0 }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createTraeAdapter(options) {
|
|
73
|
+
const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`));
|
|
74
|
+
const provider = {
|
|
75
|
+
...createProvider({
|
|
76
|
+
id: TRAE_PROVIDER,
|
|
77
|
+
name: "Trae",
|
|
78
|
+
auth: { apiKey: {
|
|
79
|
+
name: "Trae loopback secret",
|
|
80
|
+
async resolve({ credential }) {
|
|
81
|
+
const apiKey = credential?.key;
|
|
82
|
+
return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
|
|
83
|
+
auth: { apiKey },
|
|
84
|
+
source: "Trae loopback"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} },
|
|
88
|
+
models: buildModels(),
|
|
89
|
+
api: openAICompletionsApi()
|
|
90
|
+
}),
|
|
91
|
+
getModels: () => buildModels()
|
|
92
|
+
};
|
|
93
|
+
const profile = {
|
|
94
|
+
provider: TRAE_PROVIDER,
|
|
95
|
+
displayName: "Trae",
|
|
96
|
+
streamIdleTimeoutMs: TRAE_STREAM_IDLE_TIMEOUT_MS,
|
|
97
|
+
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
|
|
98
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
99
|
+
...REQUEST_IMAGE_BUDGETS,
|
|
100
|
+
piProvider: provider
|
|
101
|
+
};
|
|
102
|
+
let profiles = /* @__PURE__ */ new Map([[TRAE_PROVIDER, profile]]);
|
|
103
|
+
return {
|
|
104
|
+
adapter: new PiAiAdapter({
|
|
105
|
+
profiles: () => profiles,
|
|
106
|
+
auth: INERT_AUTH,
|
|
107
|
+
resolveApiKey: async () => options.shim.token(),
|
|
108
|
+
...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
|
|
109
|
+
}),
|
|
110
|
+
invalidate() {
|
|
111
|
+
profiles = /* @__PURE__ */ new Map([[TRAE_PROVIDER, profile]]);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/decrypt.ts
|
|
117
|
+
const TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
118
|
+
const SALT_A = Uint8Array.from([
|
|
119
|
+
82,
|
|
120
|
+
9,
|
|
121
|
+
106,
|
|
122
|
+
213,
|
|
123
|
+
48,
|
|
124
|
+
54,
|
|
125
|
+
165,
|
|
126
|
+
56,
|
|
127
|
+
191,
|
|
128
|
+
64,
|
|
129
|
+
163,
|
|
130
|
+
158,
|
|
131
|
+
129,
|
|
132
|
+
243,
|
|
133
|
+
215,
|
|
134
|
+
251,
|
|
135
|
+
124,
|
|
136
|
+
227,
|
|
137
|
+
57,
|
|
138
|
+
130,
|
|
139
|
+
155,
|
|
140
|
+
47,
|
|
141
|
+
255,
|
|
142
|
+
135,
|
|
143
|
+
52,
|
|
144
|
+
142,
|
|
145
|
+
67,
|
|
146
|
+
68,
|
|
147
|
+
196,
|
|
148
|
+
222,
|
|
149
|
+
233,
|
|
150
|
+
203,
|
|
151
|
+
84,
|
|
152
|
+
123,
|
|
153
|
+
148,
|
|
154
|
+
50,
|
|
155
|
+
166,
|
|
156
|
+
194,
|
|
157
|
+
35,
|
|
158
|
+
61,
|
|
159
|
+
238,
|
|
160
|
+
76,
|
|
161
|
+
149,
|
|
162
|
+
11,
|
|
163
|
+
66,
|
|
164
|
+
250,
|
|
165
|
+
195,
|
|
166
|
+
78,
|
|
167
|
+
8,
|
|
168
|
+
46,
|
|
169
|
+
161,
|
|
170
|
+
102,
|
|
171
|
+
40,
|
|
172
|
+
217,
|
|
173
|
+
36,
|
|
174
|
+
178,
|
|
175
|
+
118,
|
|
176
|
+
91,
|
|
177
|
+
162,
|
|
178
|
+
73,
|
|
179
|
+
109,
|
|
180
|
+
139,
|
|
181
|
+
209,
|
|
182
|
+
37
|
|
183
|
+
]);
|
|
184
|
+
const SALT_B = Uint8Array.from([
|
|
185
|
+
31,
|
|
186
|
+
221,
|
|
187
|
+
168,
|
|
188
|
+
51,
|
|
189
|
+
136,
|
|
190
|
+
7,
|
|
191
|
+
199,
|
|
192
|
+
49,
|
|
193
|
+
177,
|
|
194
|
+
18,
|
|
195
|
+
16,
|
|
196
|
+
89,
|
|
197
|
+
39,
|
|
198
|
+
128,
|
|
199
|
+
236,
|
|
200
|
+
95,
|
|
201
|
+
96,
|
|
202
|
+
81,
|
|
203
|
+
127,
|
|
204
|
+
169,
|
|
205
|
+
25,
|
|
206
|
+
181,
|
|
207
|
+
74,
|
|
208
|
+
13,
|
|
209
|
+
45,
|
|
210
|
+
229,
|
|
211
|
+
122,
|
|
212
|
+
159,
|
|
213
|
+
147,
|
|
214
|
+
201,
|
|
215
|
+
156,
|
|
216
|
+
239,
|
|
217
|
+
160,
|
|
218
|
+
224,
|
|
219
|
+
59,
|
|
220
|
+
77,
|
|
221
|
+
174,
|
|
222
|
+
42,
|
|
223
|
+
245,
|
|
224
|
+
176,
|
|
225
|
+
200,
|
|
226
|
+
235,
|
|
227
|
+
187,
|
|
228
|
+
60,
|
|
229
|
+
131,
|
|
230
|
+
83,
|
|
231
|
+
153,
|
|
232
|
+
97,
|
|
233
|
+
23,
|
|
234
|
+
43,
|
|
235
|
+
4,
|
|
236
|
+
126,
|
|
237
|
+
186,
|
|
238
|
+
119,
|
|
239
|
+
214,
|
|
240
|
+
38,
|
|
241
|
+
225,
|
|
242
|
+
105,
|
|
243
|
+
20,
|
|
244
|
+
99,
|
|
245
|
+
85,
|
|
246
|
+
33,
|
|
247
|
+
12,
|
|
248
|
+
125
|
|
249
|
+
]);
|
|
250
|
+
const SALT_C = Uint8Array.from([
|
|
251
|
+
191,
|
|
252
|
+
192,
|
|
253
|
+
216,
|
|
254
|
+
250,
|
|
255
|
+
122,
|
|
256
|
+
246,
|
|
257
|
+
220,
|
|
258
|
+
97,
|
|
259
|
+
31,
|
|
260
|
+
254,
|
|
261
|
+
98,
|
|
262
|
+
27,
|
|
263
|
+
8,
|
|
264
|
+
72,
|
|
265
|
+
71,
|
|
266
|
+
176,
|
|
267
|
+
135,
|
|
268
|
+
99,
|
|
269
|
+
96,
|
|
270
|
+
18,
|
|
271
|
+
127,
|
|
272
|
+
101,
|
|
273
|
+
203,
|
|
274
|
+
104,
|
|
275
|
+
211,
|
|
276
|
+
102,
|
|
277
|
+
191,
|
|
278
|
+
125,
|
|
279
|
+
37,
|
|
280
|
+
72,
|
|
281
|
+
150,
|
|
282
|
+
156,
|
|
283
|
+
51,
|
|
284
|
+
229,
|
|
285
|
+
121,
|
|
286
|
+
35,
|
|
287
|
+
17,
|
|
288
|
+
153,
|
|
289
|
+
141,
|
|
290
|
+
177,
|
|
291
|
+
110,
|
|
292
|
+
131,
|
|
293
|
+
150,
|
|
294
|
+
128,
|
|
295
|
+
172,
|
|
296
|
+
255,
|
|
297
|
+
254,
|
|
298
|
+
6,
|
|
299
|
+
18,
|
|
300
|
+
140,
|
|
301
|
+
55,
|
|
302
|
+
62,
|
|
303
|
+
236,
|
|
304
|
+
249,
|
|
305
|
+
135,
|
|
306
|
+
64,
|
|
307
|
+
135,
|
|
308
|
+
12,
|
|
309
|
+
117,
|
|
310
|
+
4,
|
|
311
|
+
89,
|
|
312
|
+
149,
|
|
313
|
+
168,
|
|
314
|
+
209
|
|
315
|
+
]);
|
|
316
|
+
const SALT_D = Uint8Array.from([
|
|
317
|
+
246,
|
|
318
|
+
204,
|
|
319
|
+
26,
|
|
320
|
+
232,
|
|
321
|
+
232,
|
|
322
|
+
70,
|
|
323
|
+
129,
|
|
324
|
+
109,
|
|
325
|
+
223,
|
|
326
|
+
146,
|
|
327
|
+
169,
|
|
328
|
+
242,
|
|
329
|
+
23,
|
|
330
|
+
241,
|
|
331
|
+
105,
|
|
332
|
+
145,
|
|
333
|
+
50,
|
|
334
|
+
196,
|
|
335
|
+
165,
|
|
336
|
+
42,
|
|
337
|
+
254,
|
|
338
|
+
120,
|
|
339
|
+
3,
|
|
340
|
+
54,
|
|
341
|
+
244,
|
|
342
|
+
207,
|
|
343
|
+
209,
|
|
344
|
+
85,
|
|
345
|
+
53,
|
|
346
|
+
6,
|
|
347
|
+
138,
|
|
348
|
+
106,
|
|
349
|
+
175,
|
|
350
|
+
148,
|
|
351
|
+
31,
|
|
352
|
+
204,
|
|
353
|
+
186,
|
|
354
|
+
186,
|
|
355
|
+
165,
|
|
356
|
+
182,
|
|
357
|
+
87,
|
|
358
|
+
142,
|
|
359
|
+
49,
|
|
360
|
+
10,
|
|
361
|
+
39,
|
|
362
|
+
110,
|
|
363
|
+
26,
|
|
364
|
+
154,
|
|
365
|
+
86,
|
|
366
|
+
56,
|
|
367
|
+
173,
|
|
368
|
+
125,
|
|
369
|
+
18,
|
|
370
|
+
64,
|
|
371
|
+
198,
|
|
372
|
+
225,
|
|
373
|
+
99,
|
|
374
|
+
99,
|
|
375
|
+
83,
|
|
376
|
+
82,
|
|
377
|
+
191,
|
|
378
|
+
134,
|
|
379
|
+
76,
|
|
380
|
+
170
|
|
381
|
+
]);
|
|
382
|
+
function xor(a, b) {
|
|
383
|
+
return Buffer.from(a.map((value, index) => value ^ (b[index] ?? 0)));
|
|
384
|
+
}
|
|
385
|
+
function encryptionType(header) {
|
|
386
|
+
if (header.equals(Buffer.from([
|
|
387
|
+
116,
|
|
388
|
+
99,
|
|
389
|
+
5,
|
|
390
|
+
16,
|
|
391
|
+
0,
|
|
392
|
+
0
|
|
393
|
+
]))) return "aes";
|
|
394
|
+
if (header.equals(Buffer.from([
|
|
395
|
+
18,
|
|
396
|
+
57,
|
|
397
|
+
32,
|
|
398
|
+
32,
|
|
399
|
+
2,
|
|
400
|
+
3
|
|
401
|
+
]))) return "aes-private";
|
|
402
|
+
throw new Error("unsupported Trae auth encryption header");
|
|
403
|
+
}
|
|
404
|
+
function decryptTraeStorageValue(encoded) {
|
|
405
|
+
const buffer = Buffer.from(encoded, "base64");
|
|
406
|
+
if (buffer.length <= 102) throw new Error("Trae auth ciphertext is too short");
|
|
407
|
+
const type = encryptionType(buffer.subarray(0, 6));
|
|
408
|
+
const random = buffer.subarray(6, 38);
|
|
409
|
+
const encrypted = buffer.subarray(38);
|
|
410
|
+
const salt = type === "aes-private" ? xor(SALT_C, SALT_D) : xor(SALT_A, SALT_B);
|
|
411
|
+
const first = createHash("sha512").update(random).digest();
|
|
412
|
+
const derived = createHash("sha512").update(Buffer.concat([first, salt])).digest();
|
|
413
|
+
const decipher = createDecipheriv("aes-128-cbc", derived.subarray(0, 16), derived.subarray(16, 32));
|
|
414
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
415
|
+
if (decrypted.length < 64) throw new Error("Trae auth plaintext is too short");
|
|
416
|
+
const expected = decrypted.subarray(0, 64);
|
|
417
|
+
const plaintext = decrypted.subarray(64);
|
|
418
|
+
const actual = createHash("sha512").update(plaintext).digest();
|
|
419
|
+
if (!expected.equals(actual)) throw new Error("Trae auth integrity check failed");
|
|
420
|
+
return plaintext.toString("utf8");
|
|
421
|
+
}
|
|
422
|
+
function parseTraeAuthValue(value) {
|
|
423
|
+
const trimmed = value.trim();
|
|
424
|
+
if (trimmed === "") throw new Error("Trae auth value is empty");
|
|
425
|
+
const plaintext = trimmed.startsWith("{") ? trimmed : decryptTraeStorageValue(trimmed);
|
|
426
|
+
return JSON.parse(plaintext);
|
|
427
|
+
}
|
|
428
|
+
function parseTraeStorageDocument(text) {
|
|
429
|
+
const parsed = JSON.parse(text);
|
|
430
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Trae storage document must be an object");
|
|
431
|
+
const value = parsed[TRAE_AUTH_STORAGE_KEY];
|
|
432
|
+
if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
|
|
433
|
+
return parseTraeAuthValue(value);
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/paths.ts
|
|
437
|
+
const APP_NAMES = {
|
|
438
|
+
cn: "Trae CN",
|
|
439
|
+
sg: "Trae",
|
|
440
|
+
solo: "TRAE SOLO CN",
|
|
441
|
+
"solo-sg": "TRAE SOLO"
|
|
442
|
+
};
|
|
443
|
+
function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
|
|
444
|
+
const result = [];
|
|
445
|
+
for (const edition of [
|
|
446
|
+
"cn",
|
|
447
|
+
"sg",
|
|
448
|
+
"solo",
|
|
449
|
+
"solo-sg"
|
|
450
|
+
]) {
|
|
451
|
+
const app = APP_NAMES[edition];
|
|
452
|
+
let roots;
|
|
453
|
+
if (platform === "darwin") roots = [join(home, "Library", "Application Support")];
|
|
454
|
+
else if (platform === "win32") roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
|
|
455
|
+
else if (platform === "linux") roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
|
|
456
|
+
else roots = [];
|
|
457
|
+
for (const root of roots) result.push({
|
|
458
|
+
edition,
|
|
459
|
+
path: join(root, app, "User", "globalStorage", "storage.json")
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/auth.ts
|
|
466
|
+
const OWN_VERSION = 1;
|
|
467
|
+
const TRAE_AUTH_FILENAME = ".trae-auth.json";
|
|
468
|
+
function traeOwnAuthPath() {
|
|
469
|
+
return join(resolveDshHome(), TRAE_AUTH_FILENAME);
|
|
470
|
+
}
|
|
471
|
+
function optionalString(value) {
|
|
472
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
473
|
+
}
|
|
474
|
+
function timeToMs(value) {
|
|
475
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value > 0xe8d4a51000 ? value : value * 1e3;
|
|
476
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
477
|
+
const numeric = Number(value);
|
|
478
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric > 0xe8d4a51000 ? numeric : numeric * 1e3;
|
|
479
|
+
const parsed = Date.parse(value);
|
|
480
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
481
|
+
}
|
|
482
|
+
function normalizeTraeCredential(raw, edition, source) {
|
|
483
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
484
|
+
const value = raw;
|
|
485
|
+
const accessToken = optionalString(value["token"]) ?? optionalString(value["accessToken"]);
|
|
486
|
+
if (accessToken === void 0) return void 0;
|
|
487
|
+
const expiresAtMs = timeToMs(value["expiredAt"] ?? value["expiresAt"]) ?? 0;
|
|
488
|
+
const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
|
|
489
|
+
const refreshToken = optionalString(value["refreshToken"]);
|
|
490
|
+
const accountName = optionalString((typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0)?.["username"]);
|
|
491
|
+
return {
|
|
492
|
+
accessToken,
|
|
493
|
+
...refreshToken === void 0 ? {} : { refreshToken },
|
|
494
|
+
userId: optionalString(value["userId"]) ?? "",
|
|
495
|
+
...accountName === void 0 ? {} : { accountName },
|
|
496
|
+
host: optionalString(value["host"]) ?? "",
|
|
497
|
+
expiresAtMs,
|
|
498
|
+
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
499
|
+
edition,
|
|
500
|
+
source
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function traeAccountId(credential) {
|
|
504
|
+
const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
|
|
505
|
+
return createHash("sha256").update(stable).digest("hex").slice(0, 24);
|
|
506
|
+
}
|
|
507
|
+
function parseOwn(text) {
|
|
508
|
+
try {
|
|
509
|
+
const document = JSON.parse(text);
|
|
510
|
+
if (document.version !== OWN_VERSION || typeof document.credential !== "object" || document.credential === null) return void 0;
|
|
511
|
+
const stored = document.credential;
|
|
512
|
+
const edition = stored["edition"];
|
|
513
|
+
if (edition !== "cn" && edition !== "sg" && edition !== "solo" && edition !== "solo-sg") return void 0;
|
|
514
|
+
return normalizeTraeCredential({
|
|
515
|
+
token: stored["accessToken"],
|
|
516
|
+
refreshToken: stored["refreshToken"],
|
|
517
|
+
userId: stored["userId"],
|
|
518
|
+
host: stored["host"],
|
|
519
|
+
account: stored["accountName"] === void 0 ? void 0 : { username: stored["accountName"] },
|
|
520
|
+
expiredAt: stored["expiresAtMs"],
|
|
521
|
+
refreshExpiredAt: stored["refreshExpiresAtMs"]
|
|
522
|
+
}, edition, "dsh");
|
|
523
|
+
} catch {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
var TraeCredentialStore = class {
|
|
528
|
+
storagePathOverride;
|
|
529
|
+
edition;
|
|
530
|
+
accountId;
|
|
531
|
+
preferIds = [];
|
|
532
|
+
ownPath;
|
|
533
|
+
refresh;
|
|
534
|
+
refreshMarginMs;
|
|
535
|
+
inflight;
|
|
536
|
+
constructor(options) {
|
|
537
|
+
this.storagePathOverride = options.storagePath;
|
|
538
|
+
this.edition = options.edition ?? "auto";
|
|
539
|
+
this.accountId = options.accountId;
|
|
540
|
+
this.ownPath = options.ownPath ?? traeOwnAuthPath();
|
|
541
|
+
this.refresh = options.refresh;
|
|
542
|
+
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
543
|
+
}
|
|
544
|
+
setSource(storagePath, edition = "auto", accountId) {
|
|
545
|
+
this.storagePathOverride = storagePath;
|
|
546
|
+
this.edition = edition;
|
|
547
|
+
this.accountId = accountId;
|
|
548
|
+
this.inflight = void 0;
|
|
549
|
+
}
|
|
550
|
+
selectAccount(accountId) {
|
|
551
|
+
this.accountId = accountId;
|
|
552
|
+
this.inflight = void 0;
|
|
553
|
+
}
|
|
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
|
+
candidates() {
|
|
565
|
+
if (this.storagePathOverride !== void 0) return [{
|
|
566
|
+
edition: this.edition === "auto" ? "cn" : this.edition,
|
|
567
|
+
path: this.storagePathOverride
|
|
568
|
+
}];
|
|
569
|
+
const all = traeStorageCandidates();
|
|
570
|
+
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
|
+
}
|
|
572
|
+
/** First credential matching the preferred ordering, else the first live one. */
|
|
573
|
+
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
|
+
return credentials[0];
|
|
580
|
+
}
|
|
581
|
+
async accounts() {
|
|
582
|
+
const credentials = await this.readAll();
|
|
583
|
+
const selectedExists = this.accountId !== void 0 && credentials.some((credential) => traeAccountId(credential) === this.accountId);
|
|
584
|
+
const defaultSelected = this.preferred(credentials);
|
|
585
|
+
return credentials.map((credential) => ({
|
|
586
|
+
id: traeAccountId(credential),
|
|
587
|
+
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
|
|
588
|
+
edition: credential.edition,
|
|
589
|
+
source: credential.source,
|
|
590
|
+
tokenExpiresAtMs: credential.expiresAtMs,
|
|
591
|
+
selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
async current() {
|
|
595
|
+
const credentials = await this.readAll();
|
|
596
|
+
if (this.accountId === void 0) return this.preferred(credentials);
|
|
597
|
+
return credentials.find((credential) => traeAccountId(credential) === this.accountId) ?? this.preferred(credentials);
|
|
598
|
+
}
|
|
599
|
+
async resolve() {
|
|
600
|
+
const credential = await this.current();
|
|
601
|
+
if (credential === void 0) throw new Error(`trae: no signed-in account found (${this.candidates().map((item) => item.path).join(" or ")})`);
|
|
602
|
+
if (credential.expiresAtMs > Date.now() + this.refreshMarginMs) return credential;
|
|
603
|
+
this.inflight ??= this.refreshNow(credential).finally(() => {
|
|
604
|
+
this.inflight = void 0;
|
|
605
|
+
});
|
|
606
|
+
return this.inflight;
|
|
607
|
+
}
|
|
608
|
+
async status() {
|
|
609
|
+
try {
|
|
610
|
+
const value = await this.current();
|
|
611
|
+
return value === void 0 ? { state: "signed-out" } : {
|
|
612
|
+
state: "signed-in",
|
|
613
|
+
edition: value.edition,
|
|
614
|
+
expiresAtMs: value.expiresAtMs,
|
|
615
|
+
source: value.source
|
|
616
|
+
};
|
|
617
|
+
} catch {
|
|
618
|
+
return { state: "signed-out" };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async desktopFilePresent() {
|
|
622
|
+
for (const candidate of this.candidates()) try {
|
|
623
|
+
if ((await stat(candidate.path)).isFile()) return true;
|
|
624
|
+
} catch {}
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
async logout() {
|
|
628
|
+
await rm(this.ownPath, { force: true });
|
|
629
|
+
await rm(`${this.ownPath}.lock`, { force: true });
|
|
630
|
+
}
|
|
631
|
+
async readAll() {
|
|
632
|
+
const desktop = await this.readDesktopAll();
|
|
633
|
+
const own = await this.readOwn();
|
|
634
|
+
const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
|
|
635
|
+
if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
|
|
636
|
+
return [...desktop, cnOwn];
|
|
637
|
+
}
|
|
638
|
+
async readDesktopAll() {
|
|
639
|
+
const credentials = [];
|
|
640
|
+
for (const candidate of this.candidates()) try {
|
|
641
|
+
const credential = normalizeTraeCredential(parseTraeStorageDocument(await readFile(candidate.path, "utf8")), candidate.edition, "desktop");
|
|
642
|
+
if (credential !== void 0 && !credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
|
|
643
|
+
} catch {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
return credentials;
|
|
647
|
+
}
|
|
648
|
+
async readOwn() {
|
|
649
|
+
try {
|
|
650
|
+
return parseOwn(await readFile(this.ownPath, "utf8"));
|
|
651
|
+
} catch {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async refreshNow(credential) {
|
|
656
|
+
if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
|
|
657
|
+
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
658
|
+
throw new Error("trae: access token expired and no valid refresh token is available; sign in again in Trae");
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const outcome = await this.refresh(credential);
|
|
662
|
+
const refreshed = {
|
|
663
|
+
...credential,
|
|
664
|
+
accessToken: outcome.accessToken,
|
|
665
|
+
...outcome.refreshToken === void 0 ? {} : { refreshToken: outcome.refreshToken },
|
|
666
|
+
expiresAtMs: outcome.expiresAtMs,
|
|
667
|
+
...outcome.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: outcome.refreshExpiresAtMs },
|
|
668
|
+
...outcome.host === void 0 ? {} : { host: outcome.host },
|
|
669
|
+
source: "dsh"
|
|
670
|
+
};
|
|
671
|
+
await withFileLock(this.ownPath, async () => {
|
|
672
|
+
await writeFileAtomic(this.ownPath, `${JSON.stringify({
|
|
673
|
+
version: OWN_VERSION,
|
|
674
|
+
credential: refreshed
|
|
675
|
+
}, null, 2)}\n`, {
|
|
676
|
+
mode: 384,
|
|
677
|
+
dirMode: 448
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
return refreshed;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
683
|
+
throw new Error(`trae: token refresh failed and access token is expired (${String(error)}); sign in again in Trae`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
//#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
|
+
//#region src/refresh.ts
|
|
787
|
+
const CLIENT_ID_BY_EDITION = {
|
|
788
|
+
cn: "ono9krqynydwx5",
|
|
789
|
+
solo: "ono9krqynydwx5"
|
|
790
|
+
};
|
|
791
|
+
function normalizeHost(host) {
|
|
792
|
+
const value = host.trim();
|
|
793
|
+
if (value === "") throw new Error("Trae refresh host is missing");
|
|
794
|
+
return value.replace(/\/$/, "");
|
|
795
|
+
}
|
|
796
|
+
/** Refresh only editions whose client contract is currently evidenced. */
|
|
797
|
+
async function refreshTraeCredential(credential, signal) {
|
|
798
|
+
const clientId = CLIENT_ID_BY_EDITION[credential.edition];
|
|
799
|
+
if (clientId === void 0) throw new Error(`Trae ${credential.edition} refresh contract is not verified`);
|
|
800
|
+
if (credential.refreshToken === void 0) throw new Error("Trae refresh token is missing");
|
|
801
|
+
const response = await fetch(`${normalizeHost(credential.host)}/cloudide/api/v3/trae/oauth/ExchangeToken`, {
|
|
802
|
+
method: "POST",
|
|
803
|
+
headers: { "Content-Type": "application/json" },
|
|
804
|
+
body: JSON.stringify({
|
|
805
|
+
ClientID: clientId,
|
|
806
|
+
RefreshToken: credential.refreshToken,
|
|
807
|
+
ClientSecret: "-",
|
|
808
|
+
UserID: credential.userId
|
|
809
|
+
}),
|
|
810
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
811
|
+
});
|
|
812
|
+
if (!response.ok) throw new Error(`Trae token refresh failed (http ${response.status})`);
|
|
813
|
+
const result = (await response.json()).Result;
|
|
814
|
+
const accessToken = typeof result?.["Token"] === "string" ? result["Token"] : "";
|
|
815
|
+
if (accessToken === "") throw new Error("Trae token refresh returned no token");
|
|
816
|
+
const expiry = result?.["TokenExpireAt"];
|
|
817
|
+
const expiresAtMs = typeof expiry === "number" ? expiry : typeof expiry === "string" ? Date.parse(expiry) : NaN;
|
|
818
|
+
if (!Number.isFinite(expiresAtMs)) throw new Error("Trae token refresh returned an invalid expiry");
|
|
819
|
+
const refreshToken = typeof result?.["RefreshToken"] === "string" && result["RefreshToken"] !== "" ? result["RefreshToken"] : void 0;
|
|
820
|
+
return {
|
|
821
|
+
accessToken,
|
|
822
|
+
...refreshToken === void 0 ? {} : { refreshToken },
|
|
823
|
+
expiresAtMs
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/identity.ts
|
|
828
|
+
function nonEmpty(value) {
|
|
829
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
830
|
+
}
|
|
831
|
+
function deviceCenterId(storage) {
|
|
832
|
+
const prefix = "iCubeAuthInfo://icube-dc:";
|
|
833
|
+
const ids = Object.keys(storage).filter((key) => key.startsWith(prefix)).map((key) => key.slice(25)).filter(Boolean);
|
|
834
|
+
return ids.length === 1 ? ids[0] : void 0;
|
|
835
|
+
}
|
|
836
|
+
/** Read stable identity from Trae-owned files without generating impersonated IDs. */
|
|
837
|
+
async function readTraeIdentity(candidate) {
|
|
838
|
+
const storage = JSON.parse(await readFile(candidate.path, "utf8"));
|
|
839
|
+
const appRoot = dirname(dirname(dirname(candidate.path)));
|
|
840
|
+
const machineFile = nonEmpty(await readFile(join(appRoot, "machineid"), "utf8").catch(() => ""));
|
|
841
|
+
const telemetryMachine = nonEmpty(storage["telemetry.machineId"]);
|
|
842
|
+
const devDevice = nonEmpty(storage["telemetry.devDeviceId"]);
|
|
843
|
+
const dcDevice = deviceCenterId(storage);
|
|
844
|
+
const machineId = telemetryMachine ?? machineFile;
|
|
845
|
+
if (machineId === void 0) throw new Error(`Trae ${candidate.edition} has no stable machine identity`);
|
|
846
|
+
const deviceId = dcDevice ?? devDevice ?? createHash("sha256").update(machineId).digest("hex").slice(0, 32);
|
|
847
|
+
const buildVersion = nonEmpty(storage["iCubeLastVersion"]);
|
|
848
|
+
const productPaths = candidate.edition === "cn" || candidate.edition === "solo" ? [candidate.edition === "cn" ? "/Applications/Trae CN.app/Contents/Resources/app/product.json" : "/Applications/TRAE SOLO CN.app/Contents/Resources/app/product.json"] : [];
|
|
849
|
+
let product = {};
|
|
850
|
+
for (const path of productPaths) try {
|
|
851
|
+
product = JSON.parse(await readFile(path, "utf8"));
|
|
852
|
+
break;
|
|
853
|
+
} catch {}
|
|
854
|
+
const appVersion = nonEmpty(product["appVersion"]);
|
|
855
|
+
const deviceBrand = process.platform === "darwin" ? nonEmpty(process.env["TRAE_DEVICE_BRAND"]) : void 0;
|
|
856
|
+
const deviceCpu = cpus()[0]?.model.split(" ")[0];
|
|
857
|
+
const osVersion = `${process.platform === "darwin" ? "macOS" : process.platform} ${release()}`;
|
|
858
|
+
return {
|
|
859
|
+
edition: candidate.edition,
|
|
860
|
+
machineId,
|
|
861
|
+
deviceId,
|
|
862
|
+
...appVersion === void 0 ? {} : { appVersion },
|
|
863
|
+
...buildVersion === void 0 ? {} : { buildVersion },
|
|
864
|
+
...deviceBrand === void 0 ? {} : { deviceBrand },
|
|
865
|
+
...deviceCpu === void 0 ? {} : { deviceCpu },
|
|
866
|
+
osVersion,
|
|
867
|
+
platform: process.platform
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
/** Headers derived from actual persisted identity, never a new random identity per request. */
|
|
871
|
+
function identityHeaders(identity) {
|
|
872
|
+
return {
|
|
873
|
+
"x-machine-id": identity.machineId,
|
|
874
|
+
"x-device-id": identity.deviceId,
|
|
875
|
+
"x-device-type": identity.platform === "darwin" ? "mac" : identity.platform === "win32" ? "windows" : identity.platform,
|
|
876
|
+
...identity.deviceBrand === void 0 ? {} : { "x-device-brand": identity.deviceBrand },
|
|
877
|
+
...identity.deviceCpu === void 0 ? {} : { "x-device-cpu": identity.deviceCpu },
|
|
878
|
+
...identity.osVersion === void 0 ? {} : { "x-os-version": identity.osVersion },
|
|
879
|
+
...identity.appVersion === void 0 ? {} : {
|
|
880
|
+
"x-app-version": identity.appVersion,
|
|
881
|
+
"x-ide-version": identity.appVersion
|
|
882
|
+
},
|
|
883
|
+
...identity.buildVersion === void 0 ? {} : {
|
|
884
|
+
"x-app-version-code": identity.buildVersion,
|
|
885
|
+
"x-ide-version-code": identity.buildVersion
|
|
886
|
+
},
|
|
887
|
+
"x-ide-version-type": "stable"
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/shim.ts
|
|
892
|
+
const BODY_LIMIT = 67108864;
|
|
893
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
894
|
+
"127.0.0.1",
|
|
895
|
+
"localhost",
|
|
896
|
+
"[::1]"
|
|
897
|
+
]);
|
|
898
|
+
const STATUS_BY_KIND = {
|
|
899
|
+
authentication: 401,
|
|
900
|
+
hard_credit: 402,
|
|
901
|
+
soft_rate: 429,
|
|
902
|
+
not_found: 502,
|
|
903
|
+
server: 502,
|
|
904
|
+
client: 400,
|
|
905
|
+
unconfigured: 503
|
|
906
|
+
};
|
|
907
|
+
function hostnameOfHost(host) {
|
|
908
|
+
let hostname = host.trim().toLowerCase();
|
|
909
|
+
if (hostname.startsWith("[")) {
|
|
910
|
+
const end = hostname.indexOf("]");
|
|
911
|
+
return end === -1 ? hostname : hostname.slice(0, end + 1);
|
|
912
|
+
}
|
|
913
|
+
const colon = hostname.lastIndexOf(":");
|
|
914
|
+
if (colon !== -1 && /^\d+$/.test(hostname.slice(colon + 1))) hostname = hostname.slice(0, colon);
|
|
915
|
+
return hostname;
|
|
916
|
+
}
|
|
917
|
+
function hostIsLoopback(host) {
|
|
918
|
+
return host !== void 0 && host.trim() !== "" && LOOPBACK_HOSTS.has(hostnameOfHost(host));
|
|
919
|
+
}
|
|
920
|
+
function originIsLoopback(origin) {
|
|
921
|
+
if (origin === void 0 || origin.trim() === "") return true;
|
|
922
|
+
try {
|
|
923
|
+
const hostname = new URL(origin).hostname;
|
|
924
|
+
return LOOPBACK_HOSTS.has(hostname) || hostname === "::1";
|
|
925
|
+
} catch {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function writeJson(res, status, body) {
|
|
930
|
+
const payload = JSON.stringify(body);
|
|
931
|
+
res.writeHead(status, {
|
|
932
|
+
"Content-Type": "application/json",
|
|
933
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
934
|
+
});
|
|
935
|
+
res.end(payload);
|
|
936
|
+
}
|
|
937
|
+
function writeError(res, status, kind, message) {
|
|
938
|
+
writeJson(res, status, { error: {
|
|
939
|
+
message,
|
|
940
|
+
type: kind,
|
|
941
|
+
code: kind
|
|
942
|
+
} });
|
|
943
|
+
}
|
|
944
|
+
function readBody(req) {
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
const chunks = [];
|
|
947
|
+
let size = 0;
|
|
948
|
+
req.on("data", (chunk) => {
|
|
949
|
+
size += chunk.length;
|
|
950
|
+
if (size > BODY_LIMIT) {
|
|
951
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
952
|
+
req.destroy();
|
|
953
|
+
} else chunks.push(chunk);
|
|
954
|
+
});
|
|
955
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
956
|
+
req.on("error", reject);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
function createTraeShim(options) {
|
|
960
|
+
const secret = randomBytes(32).toString("base64url");
|
|
961
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
962
|
+
const server = createServer((req, res) => {
|
|
963
|
+
handle(req, res);
|
|
964
|
+
});
|
|
965
|
+
server.on("connection", (socket) => {
|
|
966
|
+
sockets.add(socket);
|
|
967
|
+
socket.once("close", () => sockets.delete(socket));
|
|
968
|
+
});
|
|
969
|
+
const ready = new Promise((resolve, reject) => {
|
|
970
|
+
server.once("listening", resolve);
|
|
971
|
+
server.once("error", reject);
|
|
972
|
+
});
|
|
973
|
+
server.listen(0, "127.0.0.1");
|
|
974
|
+
function bearerOk(req) {
|
|
975
|
+
const match = typeof req.headers.authorization === "string" ? /^Bearer\s+(.+)$/i.exec(req.headers.authorization.trim()) : null;
|
|
976
|
+
if (match === null) return false;
|
|
977
|
+
const actual = Buffer.from(match[1] ?? "");
|
|
978
|
+
const expected = Buffer.from(secret);
|
|
979
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
980
|
+
}
|
|
981
|
+
async function handle(req, res) {
|
|
982
|
+
try {
|
|
983
|
+
if (!hostIsLoopback(req.headers.host)) return writeError(res, 403, "host_not_allowed", "Host must be loopback");
|
|
984
|
+
if (!originIsLoopback(req.headers.origin)) return writeError(res, 403, "origin_not_allowed", "Origin must be loopback");
|
|
985
|
+
if (!bearerOk(req)) return writeError(res, 401, "unauthorized", "Missing or invalid bearer");
|
|
986
|
+
const url = req.url ?? "/";
|
|
987
|
+
if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) return writeJson(res, 200, { ok: true });
|
|
988
|
+
if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) return writeJson(res, 200, {
|
|
989
|
+
object: "list",
|
|
990
|
+
data: options.catalog.current().map((model) => ({
|
|
991
|
+
id: model.id,
|
|
992
|
+
object: "model",
|
|
993
|
+
created: 0,
|
|
994
|
+
owned_by: "trae"
|
|
995
|
+
}))
|
|
996
|
+
});
|
|
997
|
+
if (req.method === "POST" && (url === "/v1/chat/completions" || url === "/v1/chat/completions/")) {
|
|
998
|
+
if (typeof req.headers["content-type"] !== "string" || !req.headers["content-type"].toLowerCase().startsWith("application/json")) return writeError(res, 415, "unsupported_media_type", "Content-Type must be application/json");
|
|
999
|
+
const raw = (await readBody(req)).toString("utf8");
|
|
1000
|
+
try {
|
|
1001
|
+
JSON.parse(raw);
|
|
1002
|
+
} catch {
|
|
1003
|
+
return writeError(res, 400, "invalid_json", "Request body must be valid JSON");
|
|
1004
|
+
}
|
|
1005
|
+
const parsed = JSON.parse(raw);
|
|
1006
|
+
options.logger?.warn("dsh-connect-trae: chat request received", {
|
|
1007
|
+
model: parsed.model,
|
|
1008
|
+
messages: Array.isArray(parsed.messages) ? parsed.messages.map((message) => typeof message === "object" && message !== null ? message["role"] ?? "?" : "?") : "(none)",
|
|
1009
|
+
toolCount: Array.isArray(parsed.tools) ? parsed.tools.length : 0,
|
|
1010
|
+
maxTokens: parsed.max_tokens,
|
|
1011
|
+
reasoningEffort: parsed.reasoning_effort,
|
|
1012
|
+
temperature: parsed.temperature,
|
|
1013
|
+
bodyBytes: raw.length
|
|
1014
|
+
});
|
|
1015
|
+
const controller = new AbortController();
|
|
1016
|
+
const abort = () => controller.abort();
|
|
1017
|
+
req.once("aborted", abort);
|
|
1018
|
+
req.socket.once("close", abort);
|
|
1019
|
+
const result = await options.client.chatStream(raw, controller.signal);
|
|
1020
|
+
if (!result.ok) return writeError(res, STATUS_BY_KIND[result.kind], result.kind, result.message);
|
|
1021
|
+
res.writeHead(200, {
|
|
1022
|
+
"Content-Type": "text/event-stream",
|
|
1023
|
+
"Cache-Control": "no-cache",
|
|
1024
|
+
"Connection": "keep-alive",
|
|
1025
|
+
"X-Accel-Buffering": "no"
|
|
1026
|
+
});
|
|
1027
|
+
const body = Readable.fromWeb(result.response.body);
|
|
1028
|
+
body.on("error", (error) => {
|
|
1029
|
+
const record = error instanceof Error ? {
|
|
1030
|
+
name: error.name,
|
|
1031
|
+
message: error.message,
|
|
1032
|
+
cause: error.cause ? String(error.cause) : void 0
|
|
1033
|
+
} : String(error);
|
|
1034
|
+
options.logger?.warn("dsh-connect-trae: upstream stream failed", record);
|
|
1035
|
+
if (!res.writableEnded) res.end();
|
|
1036
|
+
});
|
|
1037
|
+
body.pipe(res);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
writeError(res, 404, "not_found", `No such route: ${req.method} ${url}`);
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
options.logger?.error("dsh-connect-trae: shim request failed", error);
|
|
1043
|
+
if (!res.headersSent) writeError(res, 500, "internal", "Internal shim error");
|
|
1044
|
+
else if (!res.writableEnded) res.end();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
ready,
|
|
1049
|
+
baseUrl() {
|
|
1050
|
+
const address = server.address();
|
|
1051
|
+
if (address === null || typeof address === "string") throw new Error("trae shim is not listening");
|
|
1052
|
+
return `http://127.0.0.1:${address.port}`;
|
|
1053
|
+
},
|
|
1054
|
+
token: () => secret,
|
|
1055
|
+
close: () => new Promise((resolve, reject) => {
|
|
1056
|
+
for (const socket of sockets) socket.destroy();
|
|
1057
|
+
server.close((error) => error === void 0 ? resolve() : reject(error));
|
|
1058
|
+
})
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/protocol.ts
|
|
1063
|
+
const TRAE_CN_AGENT_TASK_PATH = "/api/agent/v3/create_agent_task";
|
|
1064
|
+
const TRAE_CN_TITLE_PATH = "/api/agent/v3/llm_utils_chat";
|
|
1065
|
+
/**
|
|
1066
|
+
* Evidence-bounded body draft. It is intentionally pure and offline; the
|
|
1067
|
+
* network client remains disabled until a controlled request validates it.
|
|
1068
|
+
*/
|
|
1069
|
+
function buildTraeAgentTaskBody(messages, model, options = {}) {
|
|
1070
|
+
if (messages.length === 0) throw new Error("Trae agent task requires at least one message");
|
|
1071
|
+
const requestId = options.requestId ?? randomUUID();
|
|
1072
|
+
const sessionId = options.sessionId ?? requestId;
|
|
1073
|
+
return {
|
|
1074
|
+
messages: messages.map((message) => ({
|
|
1075
|
+
role: message.role,
|
|
1076
|
+
content: [{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: message.content
|
|
1079
|
+
}]
|
|
1080
|
+
})),
|
|
1081
|
+
model,
|
|
1082
|
+
function: "inline_chat",
|
|
1083
|
+
stream: true,
|
|
1084
|
+
request_id: requestId,
|
|
1085
|
+
session_id: sessionId,
|
|
1086
|
+
...options.maxTokens === void 0 ? {} : { max_tokens: options.maxTokens }
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Numeric version code sent when the persisted build version is not a plain
|
|
1091
|
+
* integer. The upstream binds `x-app-version-code` / `x-ide-version-code` as a
|
|
1092
|
+
* number: Trae stores `iCubeLastVersion` as a dotted build string (observed
|
|
1093
|
+
* `2.3.76922` on TRAE SOLO CN 0.1.56), which the server rejects with
|
|
1094
|
+
* `4001 ... expr_path=app_version_code, cause=parameter type does not match
|
|
1095
|
+
* binding data`. Verified 2026-08-29: with a numeric code the same request
|
|
1096
|
+
* returns HTTP 200 + `text/event-stream` on
|
|
1097
|
+
* `/api/agent/v3/llm_utils_chat` (`solo_work_lite`).
|
|
1098
|
+
*
|
|
1099
|
+
* Only the *format* of this field is normalised. Machine, device and app
|
|
1100
|
+
* version stay exactly as persisted — nothing here impersonates a device.
|
|
1101
|
+
*/
|
|
1102
|
+
const TRAE_VERSION_CODE_FALLBACK = "20260716";
|
|
1103
|
+
/** Keep a purely numeric build version; fall back when it cannot bind. */
|
|
1104
|
+
function normalizeTraeVersionCode(buildVersion) {
|
|
1105
|
+
if (buildVersion === void 0 || buildVersion.trim() === "") return TRAE_VERSION_CODE_FALLBACK;
|
|
1106
|
+
const trimmed = buildVersion.trim();
|
|
1107
|
+
return /^\d+$/.test(trimmed) ? trimmed : TRAE_VERSION_CODE_FALLBACK;
|
|
1108
|
+
}
|
|
1109
|
+
function buildTraeCnHeaders(credential, identity, options = {}) {
|
|
1110
|
+
if (credential.edition !== "cn" && credential.edition !== "solo") throw new Error(`Trae ${credential.edition} request contract is not verified`);
|
|
1111
|
+
const requestId = options.requestId ?? randomUUID();
|
|
1112
|
+
const traceId = requestId.replaceAll("-", "").slice(0, 32);
|
|
1113
|
+
const profile = options.profile ?? "agent-task";
|
|
1114
|
+
const common = {
|
|
1115
|
+
"Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
|
|
1116
|
+
"X-Ide-Token": credential.accessToken,
|
|
1117
|
+
"x-plugin-channel": "icube-ai",
|
|
1118
|
+
"User-Agent": `Trae/${identity.appVersion ?? identity.buildVersion ?? "unknown"}`,
|
|
1119
|
+
"x-app-id": options.appId ?? "6eefa01c-1036-4c7e-9ca5-d891f63bfcd8",
|
|
1120
|
+
...identityHeaders(identity),
|
|
1121
|
+
"x-app-version-code": normalizeTraeVersionCode(identity.buildVersion),
|
|
1122
|
+
"x-ide-version-code": normalizeTraeVersionCode(identity.buildVersion),
|
|
1123
|
+
"x-custom-trace-id": traceId,
|
|
1124
|
+
"x-flow-traceparent": `04-${traceId}-${traceId.slice(0, 16)}-01`,
|
|
1125
|
+
"request-traffic-type": "prod",
|
|
1126
|
+
"Content-Type": "application/json"
|
|
1127
|
+
};
|
|
1128
|
+
if (profile === "native-curl") return {
|
|
1129
|
+
"Content-Type": "application/json",
|
|
1130
|
+
"request-traffic-type": "prod",
|
|
1131
|
+
"x-app-id": options.appId ?? "6eefa01c-1036-4c7e-9ca5-d891f63bfcd8",
|
|
1132
|
+
...identityHeaders(identity),
|
|
1133
|
+
"x-custom-trace-id": traceId,
|
|
1134
|
+
"x-flow-traceparent": `04-${traceId}-${traceId.slice(0, 16)}-01`,
|
|
1135
|
+
"X-Ide-Token": credential.accessToken
|
|
1136
|
+
};
|
|
1137
|
+
if (profile === "model-detail") return {
|
|
1138
|
+
...common,
|
|
1139
|
+
"Accept": "application/json"
|
|
1140
|
+
};
|
|
1141
|
+
if (profile === "raw-chat") return {
|
|
1142
|
+
...common,
|
|
1143
|
+
"Accept": "text/event-stream"
|
|
1144
|
+
};
|
|
1145
|
+
return {
|
|
1146
|
+
...common,
|
|
1147
|
+
"X-Cloudide-Token": credential.accessToken,
|
|
1148
|
+
"x-uid": credential.userId,
|
|
1149
|
+
"x-request-id": requestId,
|
|
1150
|
+
"x-trae-request-id": requestId,
|
|
1151
|
+
"Accept": "text/event-stream"
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function traeEndpoint(baseUrl, path) {
|
|
1155
|
+
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1156
|
+
}
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/reasoning.ts
|
|
1159
|
+
const TRAE_REASONING_EFFORTS = [
|
|
1160
|
+
"minimal",
|
|
1161
|
+
"low",
|
|
1162
|
+
"medium",
|
|
1163
|
+
"high",
|
|
1164
|
+
"xhigh"
|
|
1165
|
+
];
|
|
1166
|
+
function parseReasoningCapability(value) {
|
|
1167
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1168
|
+
const record = value;
|
|
1169
|
+
const supported = (Array.isArray(record["reasoning_effort_options"]) ? record["reasoning_effort_options"] : []).filter((item) => typeof item === "string" && TRAE_REASONING_EFFORTS.includes(item));
|
|
1170
|
+
const rawDefault = record["default_reasoning_effort"];
|
|
1171
|
+
const defaultEffort = typeof rawDefault === "string" && supported.includes(rawDefault) ? rawDefault : void 0;
|
|
1172
|
+
if (supported.length === 0 && defaultEffort === void 0) return void 0;
|
|
1173
|
+
return {
|
|
1174
|
+
supported,
|
|
1175
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
/** Add effort only when the selected model advertises that exact value. */
|
|
1179
|
+
function applyReasoningEffort(body, effort, capability) {
|
|
1180
|
+
if (effort === void 0) return body;
|
|
1181
|
+
if (capability === void 0 || !capability.supported.includes(effort)) throw new Error(`Trae model does not advertise reasoning effort ${effort}`);
|
|
1182
|
+
return {
|
|
1183
|
+
...body,
|
|
1184
|
+
reasoning_effort: effort
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/solo.ts
|
|
1189
|
+
const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
1190
|
+
const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
|
|
1191
|
+
const TRAE_SOLO_MODELS_PATH = "/api/ide/v1/get_detail_param";
|
|
1192
|
+
function classify$1(status) {
|
|
1193
|
+
if (status === 401 || status === 403) return "authentication";
|
|
1194
|
+
if (status === 402) return "hard_credit";
|
|
1195
|
+
if (status === 429) return "soft_rate";
|
|
1196
|
+
if (status === 404) return "not_found";
|
|
1197
|
+
if (status >= 500) return "server";
|
|
1198
|
+
return "client";
|
|
1199
|
+
}
|
|
1200
|
+
function prepareSoloBody(source, defaultModel = "glm-5.2") {
|
|
1201
|
+
const body = JSON.parse(source);
|
|
1202
|
+
const model = typeof body["model"] === "string" && body["model"].trim() !== "" ? body["model"].trim() : defaultModel;
|
|
1203
|
+
body["model"] = model;
|
|
1204
|
+
body["config_name"] = model;
|
|
1205
|
+
body["function"] = TRAE_SOLO_FUNCTION;
|
|
1206
|
+
body["stream"] = true;
|
|
1207
|
+
if (Array.isArray(body["messages"])) for (const raw of body["messages"]) {
|
|
1208
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1209
|
+
const message = raw;
|
|
1210
|
+
if (message["role"] === "developer") message["role"] = "system";
|
|
1211
|
+
if (typeof message["content"] === "string") message["content"] = [{
|
|
1212
|
+
type: "text",
|
|
1213
|
+
text: message["content"]
|
|
1214
|
+
}];
|
|
1215
|
+
if (message["role"] === "assistant" && Array.isArray(message["tool_calls"])) for (const rawCall of message["tool_calls"]) {
|
|
1216
|
+
if (typeof rawCall !== "object" || rawCall === null) continue;
|
|
1217
|
+
const call = rawCall;
|
|
1218
|
+
if (typeof call["function"] === "object" && call["function"] !== null) {
|
|
1219
|
+
call["function_call"] = call["function"];
|
|
1220
|
+
delete call["function"];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (message["role"] === "tool") {
|
|
1224
|
+
message["role"] = "tool";
|
|
1225
|
+
if (typeof message["tool_call_id"] !== "string" || message["tool_call_id"] === "") throw new Error("Trae SOLO tool message requires tool_call_id");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (Array.isArray(body["tools"])) for (const raw of body["tools"]) {
|
|
1229
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1230
|
+
const fn = raw["function"];
|
|
1231
|
+
if (typeof fn !== "object" || fn === null) continue;
|
|
1232
|
+
const record = fn;
|
|
1233
|
+
if (typeof record["parameters"] === "object" && record["parameters"] !== null) record["parameters"] = JSON.stringify(record["parameters"]);
|
|
1234
|
+
}
|
|
1235
|
+
return JSON.stringify(body);
|
|
1236
|
+
}
|
|
1237
|
+
var TraeSoloUpstreamClient = class {
|
|
1238
|
+
options;
|
|
1239
|
+
fetchImpl;
|
|
1240
|
+
constructor(options) {
|
|
1241
|
+
this.options = options;
|
|
1242
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1243
|
+
}
|
|
1244
|
+
async fetchModels(signal) {
|
|
1245
|
+
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
1246
|
+
const response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_SOLO_MODELS_PATH), {
|
|
1247
|
+
method: "POST",
|
|
1248
|
+
headers: {
|
|
1249
|
+
...buildTraeCnHeaders(credential, identity),
|
|
1250
|
+
Accept: "application/json"
|
|
1251
|
+
},
|
|
1252
|
+
body: JSON.stringify({
|
|
1253
|
+
function: TRAE_SOLO_FUNCTION,
|
|
1254
|
+
config_names: null,
|
|
1255
|
+
need_prompt: false,
|
|
1256
|
+
current_config_info: null,
|
|
1257
|
+
poly_prompt: true,
|
|
1258
|
+
mode_type: null,
|
|
1259
|
+
agent_type: null
|
|
1260
|
+
}),
|
|
1261
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1262
|
+
});
|
|
1263
|
+
if (!response.ok) throw new Error(`Trae SOLO models returned HTTP ${response.status}`);
|
|
1264
|
+
const document = await response.json();
|
|
1265
|
+
const list = Array.isArray(document["config_info_list"]) ? document["config_info_list"] : [];
|
|
1266
|
+
const models = [];
|
|
1267
|
+
for (const raw of list) {
|
|
1268
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1269
|
+
const config = raw;
|
|
1270
|
+
const id = typeof config["config_name"] === "string" ? config["config_name"] : "";
|
|
1271
|
+
if (id === "") continue;
|
|
1272
|
+
const display = typeof config["display_config"] === "object" && config["display_config"] !== null ? config["display_config"] : {};
|
|
1273
|
+
const details = Array.isArray(config["model_detail_list"]) ? config["model_detail_list"] : [];
|
|
1274
|
+
const detail = typeof details[0] === "object" && details[0] !== null ? details[0] : {};
|
|
1275
|
+
const contextWindow = typeof detail["max_input_tokens"] === "number" ? detail["max_input_tokens"] : typeof config["max_input_tokens"] === "number" ? config["max_input_tokens"] : void 0;
|
|
1276
|
+
const maxTokens = typeof detail["max_output_tokens"] === "number" ? detail["max_output_tokens"] : typeof config["max_output_tokens"] === "number" ? config["max_output_tokens"] : void 0;
|
|
1277
|
+
const reasoning = parseReasoningCapability({
|
|
1278
|
+
...config,
|
|
1279
|
+
...detail
|
|
1280
|
+
});
|
|
1281
|
+
models.push({
|
|
1282
|
+
id,
|
|
1283
|
+
name: typeof display["display_name"] === "string" && display["display_name"] !== "" ? display["display_name"] : id,
|
|
1284
|
+
...contextWindow === void 0 ? {} : { contextWindow },
|
|
1285
|
+
...maxTokens === void 0 ? {} : { maxTokens },
|
|
1286
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (models.length === 0) throw new Error("Trae SOLO models response contained no models");
|
|
1290
|
+
return models;
|
|
1291
|
+
}
|
|
1292
|
+
async chatStream(bodyJson, signal) {
|
|
1293
|
+
let prepared;
|
|
1294
|
+
try {
|
|
1295
|
+
prepared = prepareSoloBody(bodyJson);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return {
|
|
1298
|
+
ok: false,
|
|
1299
|
+
status: 400,
|
|
1300
|
+
kind: "client",
|
|
1301
|
+
message: "invalid JSON request"
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
1305
|
+
const headers = buildTraeCnHeaders(credential, identity);
|
|
1306
|
+
let response;
|
|
1307
|
+
try {
|
|
1308
|
+
response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_SOLO_CHAT_PATH), {
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
headers,
|
|
1311
|
+
body: prepared,
|
|
1312
|
+
signal: signal ?? AbortSignal.timeout(12e4)
|
|
1313
|
+
});
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
return {
|
|
1316
|
+
ok: false,
|
|
1317
|
+
status: 0,
|
|
1318
|
+
kind: "server",
|
|
1319
|
+
message: `transport error: ${String(error)}`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (response.ok) return {
|
|
1323
|
+
ok: true,
|
|
1324
|
+
response
|
|
1325
|
+
};
|
|
1326
|
+
const text = (await response.text()).slice(0, 1024);
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
status: response.status,
|
|
1330
|
+
kind: classify$1(response.status),
|
|
1331
|
+
message: text || `Trae SOLO returned HTTP ${response.status}`
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/sse.ts
|
|
1337
|
+
/** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
|
|
1338
|
+
var SseDecoder = class {
|
|
1339
|
+
buffer = "";
|
|
1340
|
+
event;
|
|
1341
|
+
id;
|
|
1342
|
+
retry;
|
|
1343
|
+
data = [];
|
|
1344
|
+
push(chunk) {
|
|
1345
|
+
this.buffer += chunk;
|
|
1346
|
+
const events = [];
|
|
1347
|
+
while (true) {
|
|
1348
|
+
const match = /\r?\n/.exec(this.buffer);
|
|
1349
|
+
if (match === null || match.index === void 0) break;
|
|
1350
|
+
const line = this.buffer.slice(0, match.index);
|
|
1351
|
+
this.buffer = this.buffer.slice(match.index + match[0].length);
|
|
1352
|
+
const emitted = this.consumeLine(line);
|
|
1353
|
+
if (emitted !== void 0) events.push(emitted);
|
|
1354
|
+
}
|
|
1355
|
+
return events;
|
|
1356
|
+
}
|
|
1357
|
+
finish() {
|
|
1358
|
+
const events = [];
|
|
1359
|
+
if (this.buffer !== "") {
|
|
1360
|
+
const emitted = this.consumeLine(this.buffer);
|
|
1361
|
+
this.buffer = "";
|
|
1362
|
+
if (emitted !== void 0) events.push(emitted);
|
|
1363
|
+
}
|
|
1364
|
+
const final = this.dispatch();
|
|
1365
|
+
if (final !== void 0) events.push(final);
|
|
1366
|
+
return events;
|
|
1367
|
+
}
|
|
1368
|
+
consumeLine(line) {
|
|
1369
|
+
if (line === "") return this.dispatch();
|
|
1370
|
+
if (line.startsWith(":")) return void 0;
|
|
1371
|
+
const colon = line.indexOf(":");
|
|
1372
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
1373
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1374
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1375
|
+
if (field === "event") this.event = value;
|
|
1376
|
+
else if (field === "data") this.data.push(value);
|
|
1377
|
+
else if (field === "id" && !value.includes("\0")) this.id = value;
|
|
1378
|
+
else if (field === "retry" && /^\d+$/.test(value)) this.retry = Number(value);
|
|
1379
|
+
}
|
|
1380
|
+
dispatch() {
|
|
1381
|
+
if (this.data.length === 0) {
|
|
1382
|
+
this.event = void 0;
|
|
1383
|
+
this.retry = void 0;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const result = {
|
|
1387
|
+
...this.event === void 0 || this.event === "" ? {} : { event: this.event },
|
|
1388
|
+
data: this.data.join("\n"),
|
|
1389
|
+
...this.id === void 0 ? {} : { id: this.id },
|
|
1390
|
+
...this.retry === void 0 ? {} : { retry: this.retry }
|
|
1391
|
+
};
|
|
1392
|
+
this.event = void 0;
|
|
1393
|
+
this.retry = void 0;
|
|
1394
|
+
this.data = [];
|
|
1395
|
+
return result;
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
function decodeTraeEvent(event) {
|
|
1399
|
+
if (event.data === "[DONE]") return {
|
|
1400
|
+
type: "done",
|
|
1401
|
+
finishReason: "stop"
|
|
1402
|
+
};
|
|
1403
|
+
let payload;
|
|
1404
|
+
try {
|
|
1405
|
+
payload = JSON.parse(event.data);
|
|
1406
|
+
} catch {
|
|
1407
|
+
return {
|
|
1408
|
+
type: "unknown",
|
|
1409
|
+
...event.event === void 0 ? {} : { event: event.event },
|
|
1410
|
+
data: event.data
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
const record = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : {};
|
|
1414
|
+
if (event.event === "request_wait_in_queue") return {
|
|
1415
|
+
type: "queue",
|
|
1416
|
+
...typeof record["position"] === "number" ? { position: record["position"] } : {}
|
|
1417
|
+
};
|
|
1418
|
+
if (event.event === "progress_notice") return {
|
|
1419
|
+
type: "progress",
|
|
1420
|
+
notice: payload
|
|
1421
|
+
};
|
|
1422
|
+
if (event.event === "token_usage") return {
|
|
1423
|
+
type: "usage",
|
|
1424
|
+
...typeof record["prompt_tokens"] === "number" ? { inputTokens: record["prompt_tokens"] } : {},
|
|
1425
|
+
...typeof record["completion_tokens"] === "number" ? { outputTokens: record["completion_tokens"] } : {},
|
|
1426
|
+
...typeof record["total_tokens"] === "number" ? { totalTokens: record["total_tokens"] } : {},
|
|
1427
|
+
...typeof record["reasoning_tokens"] === "number" ? { reasoningTokens: record["reasoning_tokens"] } : {}
|
|
1428
|
+
};
|
|
1429
|
+
if (event.event === "done" || typeof record["finish_reason"] === "string" && record["response"] === void 0) return {
|
|
1430
|
+
type: "done",
|
|
1431
|
+
finishReason: typeof record["finish_reason"] === "string" ? record["finish_reason"] : "stop"
|
|
1432
|
+
};
|
|
1433
|
+
if (event.event === "output" || record["response"] !== void 0 || record["reasoning_content"] !== void 0) return {
|
|
1434
|
+
type: "delta",
|
|
1435
|
+
text: typeof record["response"] === "string" ? record["response"] : "",
|
|
1436
|
+
...typeof record["reasoning_content"] === "string" ? { reasoning: record["reasoning_content"] } : {},
|
|
1437
|
+
...record["tool_calls"] === void 0 || record["tool_calls"] === null ? {} : { toolCalls: record["tool_calls"] }
|
|
1438
|
+
};
|
|
1439
|
+
return {
|
|
1440
|
+
type: "unknown",
|
|
1441
|
+
...event.event === void 0 ? {} : { event: event.event },
|
|
1442
|
+
data: payload
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region src/solo-bridge.ts
|
|
1447
|
+
function normalizeToolCalls(value) {
|
|
1448
|
+
if (!Array.isArray(value)) return [];
|
|
1449
|
+
const calls = [];
|
|
1450
|
+
for (const raw of value) {
|
|
1451
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1452
|
+
const record = raw;
|
|
1453
|
+
const rawFunction = typeof record["function_call"] === "object" && record["function_call"] !== null ? record["function_call"] : typeof record["function"] === "object" && record["function"] !== null ? record["function"] : {};
|
|
1454
|
+
const fn = {
|
|
1455
|
+
...typeof rawFunction["name"] === "string" ? { name: rawFunction["name"] } : {},
|
|
1456
|
+
...typeof rawFunction["arguments"] === "string" ? { arguments: rawFunction["arguments"] } : {}
|
|
1457
|
+
};
|
|
1458
|
+
calls.push({
|
|
1459
|
+
index: typeof record["index"] === "number" ? record["index"] : calls.length,
|
|
1460
|
+
...typeof record["id"] === "string" ? { id: record["id"] } : {},
|
|
1461
|
+
...record["type"] === "function" ? { type: "function" } : {},
|
|
1462
|
+
...Object.keys(fn).length === 0 ? {} : { function: fn }
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
return calls;
|
|
1466
|
+
}
|
|
1467
|
+
/** Convert Trae's named SSE events into OpenAI chat-completion SSE chunks. */
|
|
1468
|
+
function bridgeTraeSoloStream(response, model) {
|
|
1469
|
+
const source = response.body;
|
|
1470
|
+
if (source === null) return new Response(null, { status: 502 });
|
|
1471
|
+
const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
1472
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
1473
|
+
const decoder = new TextDecoder();
|
|
1474
|
+
const encoder = new TextEncoder();
|
|
1475
|
+
const sse = new SseDecoder();
|
|
1476
|
+
let sawToolCalls = false;
|
|
1477
|
+
let emittedDone = false;
|
|
1478
|
+
let upstreamError;
|
|
1479
|
+
let usage;
|
|
1480
|
+
const chunk = (delta, finishReason = null) => encoder.encode(`data: ${JSON.stringify({
|
|
1481
|
+
id,
|
|
1482
|
+
object: "chat.completion.chunk",
|
|
1483
|
+
created,
|
|
1484
|
+
model,
|
|
1485
|
+
choices: [{
|
|
1486
|
+
index: 0,
|
|
1487
|
+
delta,
|
|
1488
|
+
finish_reason: finishReason
|
|
1489
|
+
}],
|
|
1490
|
+
...usage === void 0 ? {} : { usage }
|
|
1491
|
+
})}\n\n`);
|
|
1492
|
+
const stream = new ReadableStream({
|
|
1493
|
+
async start(controller) {
|
|
1494
|
+
const reader = source.getReader();
|
|
1495
|
+
const consume = (event) => {
|
|
1496
|
+
const decoded = decodeTraeEvent(event);
|
|
1497
|
+
if (decoded.type === "unknown") {
|
|
1498
|
+
const payload = decoded.data;
|
|
1499
|
+
const code = typeof payload?.["code"] === "number" ? payload["code"] : void 0;
|
|
1500
|
+
if (decoded.event === "error" || code !== void 0 && code >= 4e3) upstreamError = new Error(typeof payload?.["message"] === "string" && payload["message"] !== "" ? payload["message"] : `Trae upstream error (code ${code ?? "?"})`);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (decoded.type === "delta") {
|
|
1504
|
+
const delta = {};
|
|
1505
|
+
if (decoded.text !== "") delta["content"] = decoded.text;
|
|
1506
|
+
if (decoded.reasoning !== void 0 && decoded.reasoning !== "") delta["reasoning_content"] = decoded.reasoning;
|
|
1507
|
+
const toolCalls = normalizeToolCalls(decoded.toolCalls);
|
|
1508
|
+
if (toolCalls.length > 0) {
|
|
1509
|
+
sawToolCalls = true;
|
|
1510
|
+
delta["tool_calls"] = toolCalls;
|
|
1511
|
+
}
|
|
1512
|
+
if (Object.keys(delta).length > 0) controller.enqueue(chunk(delta));
|
|
1513
|
+
} else if (decoded.type === "usage") usage = {
|
|
1514
|
+
...decoded.inputTokens === void 0 ? {} : { prompt_tokens: decoded.inputTokens },
|
|
1515
|
+
...decoded.outputTokens === void 0 ? {} : { completion_tokens: decoded.outputTokens },
|
|
1516
|
+
...decoded.totalTokens === void 0 ? {} : { total_tokens: decoded.totalTokens }
|
|
1517
|
+
};
|
|
1518
|
+
else if (decoded.type === "done" && !emittedDone) {
|
|
1519
|
+
emittedDone = true;
|
|
1520
|
+
if (upstreamError !== void 0) {
|
|
1521
|
+
controller.error(upstreamError);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
controller.enqueue(chunk({}, sawToolCalls ? "tool_calls" : decoded.finishReason));
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
try {
|
|
1528
|
+
while (true) {
|
|
1529
|
+
const next = await reader.read();
|
|
1530
|
+
if (next.done) break;
|
|
1531
|
+
for (const event of sse.push(decoder.decode(next.value, { stream: true }))) consume(event);
|
|
1532
|
+
}
|
|
1533
|
+
for (const event of sse.finish()) consume(event);
|
|
1534
|
+
if (upstreamError !== void 0 && !emittedDone) {
|
|
1535
|
+
controller.error(upstreamError);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (!emittedDone) controller.enqueue(chunk({}, sawToolCalls ? "tool_calls" : "stop"));
|
|
1539
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1540
|
+
controller.close();
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
controller.error(error);
|
|
1543
|
+
} finally {
|
|
1544
|
+
reader.releaseLock();
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
cancel(reason) {
|
|
1548
|
+
return source.cancel(reason);
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
return new Response(stream, {
|
|
1552
|
+
status: 200,
|
|
1553
|
+
headers: { "Content-Type": "text/event-stream" }
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
/** Native SOLO client wrapper used by the loopback OpenAI adapter. */
|
|
1557
|
+
var TraeSoloBridge = class {
|
|
1558
|
+
upstream;
|
|
1559
|
+
constructor(upstream) {
|
|
1560
|
+
this.upstream = upstream;
|
|
1561
|
+
}
|
|
1562
|
+
async chatStream(bodyJson, signal) {
|
|
1563
|
+
let model = "glm-5.2";
|
|
1564
|
+
try {
|
|
1565
|
+
const input = JSON.parse(bodyJson);
|
|
1566
|
+
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"].endsWith("@1m") ? input["model"].slice(0, -3) : input["model"];
|
|
1567
|
+
} catch {
|
|
1568
|
+
return {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
status: 400,
|
|
1571
|
+
kind: "client",
|
|
1572
|
+
message: "invalid JSON request"
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const result = await this.upstream.chatStream(bodyJson, signal);
|
|
1576
|
+
if (!result.ok) return result;
|
|
1577
|
+
return {
|
|
1578
|
+
ok: true,
|
|
1579
|
+
response: bridgeTraeSoloStream(result.response, model)
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
//#endregion
|
|
1584
|
+
//#region src/model-metadata.ts
|
|
1585
|
+
function finitePositive(value) {
|
|
1586
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1587
|
+
}
|
|
1588
|
+
function record(value) {
|
|
1589
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1590
|
+
}
|
|
1591
|
+
function parseFeatures(value) {
|
|
1592
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
1593
|
+
try {
|
|
1594
|
+
return record(JSON.parse(value));
|
|
1595
|
+
} catch {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const EFFORT_MAP = {
|
|
1600
|
+
light: "low",
|
|
1601
|
+
high: "high",
|
|
1602
|
+
extra_high: "xhigh"
|
|
1603
|
+
};
|
|
1604
|
+
/** Parse only capabilities explicitly advertised by Trae's remote model API. */
|
|
1605
|
+
function parseTraeRemoteModel(value) {
|
|
1606
|
+
const raw = record(value);
|
|
1607
|
+
if (raw === void 0 || typeof raw.name !== "string" || raw.name === "") return void 0;
|
|
1608
|
+
const context = record(raw.context_window_tokens);
|
|
1609
|
+
const dev = finitePositive(context?.["dev"]);
|
|
1610
|
+
const max = raw.max_mode === true ? finitePositive(context?.["max"]) : void 0;
|
|
1611
|
+
const features = parseFeatures(raw.features);
|
|
1612
|
+
const consumption = record(features?.["consumption_rate"]);
|
|
1613
|
+
const consumptionData = record(consumption?.["data"]);
|
|
1614
|
+
const creditMultiplier = consumption?.["enable"] === true ? finitePositive(consumptionData?.["rate"]) : void 0;
|
|
1615
|
+
const reasoningSupported = record(features?.["reasoning"])?.["enable"] === true;
|
|
1616
|
+
const reasoningConfig = record(raw.reasoning_effort_config);
|
|
1617
|
+
const supported = (Array.isArray(reasoningConfig?.["options"]) ? reasoningConfig["options"] : []).flatMap((option) => {
|
|
1618
|
+
if (typeof option !== "string") return [];
|
|
1619
|
+
const effort = EFFORT_MAP[option];
|
|
1620
|
+
return effort === void 0 ? [] : [effort];
|
|
1621
|
+
});
|
|
1622
|
+
const rawDefault = reasoningConfig?.["default_level"];
|
|
1623
|
+
const mappedDefault = typeof rawDefault === "string" ? EFFORT_MAP[rawDefault] : void 0;
|
|
1624
|
+
const defaultEffort = mappedDefault !== void 0 && supported.includes(mappedDefault) ? mappedDefault : void 0;
|
|
1625
|
+
return {
|
|
1626
|
+
id: raw.name,
|
|
1627
|
+
name: typeof raw.display_name === "string" && raw.display_name !== "" ? raw.display_name : raw.name,
|
|
1628
|
+
multimodal: raw.multimodal === true,
|
|
1629
|
+
...dev === void 0 ? {} : { contextWindow: dev },
|
|
1630
|
+
...max === void 0 ? {} : { maxContextWindow: max },
|
|
1631
|
+
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
1632
|
+
reasoningSupported,
|
|
1633
|
+
...supported.length === 0 ? {} : { reasoning: {
|
|
1634
|
+
supported,
|
|
1635
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
1636
|
+
} }
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
//#endregion
|
|
1640
|
+
//#region src/solo-remote.ts
|
|
1641
|
+
const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
|
|
1642
|
+
const TRAE_SOLO_REMOTE_MODELS = [
|
|
1643
|
+
{
|
|
1644
|
+
name: "DeepSeek-V4-Flash",
|
|
1645
|
+
displayName: "DeepSeek-V4-Flash",
|
|
1646
|
+
multimodal: false
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "DeepSeek-V4-Pro",
|
|
1650
|
+
displayName: "DeepSeek-V4-Pro",
|
|
1651
|
+
multimodal: false
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
name: "Doubao_1_6",
|
|
1655
|
+
displayName: "Doubao-Seed-Code",
|
|
1656
|
+
multimodal: true
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
name: "Doubao-Seed-2.0-Code",
|
|
1660
|
+
displayName: "Doubao-Seed-2.0-Code",
|
|
1661
|
+
multimodal: true
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
name: "kimi-k2.6",
|
|
1665
|
+
displayName: "Kimi-K2.6",
|
|
1666
|
+
multimodal: true
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
name: "kimi-k2.5",
|
|
1670
|
+
displayName: "Kimi-K2.5",
|
|
1671
|
+
multimodal: true
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
name: "qwen-3.6-plus",
|
|
1675
|
+
displayName: "Qwen3.6-Plus",
|
|
1676
|
+
multimodal: true
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: "qwen-3.5",
|
|
1680
|
+
displayName: "Qwen3.5",
|
|
1681
|
+
multimodal: true
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
name: "glm-5.1",
|
|
1685
|
+
displayName: "GLM-5.1",
|
|
1686
|
+
multimodal: false
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
name: "glm-5",
|
|
1690
|
+
displayName: "GLM-5",
|
|
1691
|
+
multimodal: false
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
name: "glm-5v-turbo",
|
|
1695
|
+
displayName: "GLM-5V-Turbo",
|
|
1696
|
+
multimodal: true
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
name: "minimax-m2.7",
|
|
1700
|
+
displayName: "MiniMax-M2.7",
|
|
1701
|
+
multimodal: false
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
name: "minimax-m2.5",
|
|
1705
|
+
displayName: "MiniMax-M2.5",
|
|
1706
|
+
multimodal: false
|
|
1707
|
+
}
|
|
1708
|
+
];
|
|
1709
|
+
/**
|
|
1710
|
+
* Preserve DSH's complete text conversation in the one query accepted by the
|
|
1711
|
+
* SOLO Remote create-session endpoint. System instructions (including DSH's
|
|
1712
|
+
* workspace/cwd context), assistant history, tool-result text and the newest
|
|
1713
|
+
* user request stay ordered instead of collapsing to the last user message.
|
|
1714
|
+
*/
|
|
1715
|
+
function serializeTraeConversation(messages) {
|
|
1716
|
+
if (messages.length === 1 && messages[0]?.role === "user") return messages[0].content;
|
|
1717
|
+
return messages.map((message) => `<dsh-message role="${message.role}">\n${message.content}\n</dsh-message>`).join("\n\n");
|
|
1718
|
+
}
|
|
1719
|
+
function decodeUserId(token) {
|
|
1720
|
+
try {
|
|
1721
|
+
return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64").toString("utf8")).data?.id ?? "0";
|
|
1722
|
+
} catch {
|
|
1723
|
+
return "0";
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function generateWebId() {
|
|
1727
|
+
return String(Math.floor(Math.random() * 9e15) + 0x38d7ea4c68000);
|
|
1728
|
+
}
|
|
1729
|
+
function buildCommonParams(userId, webId, sessionId) {
|
|
1730
|
+
return JSON.stringify({
|
|
1731
|
+
language: "zh-cn",
|
|
1732
|
+
app_language: "en",
|
|
1733
|
+
quality: "stable",
|
|
1734
|
+
app_version: "1.0.0.1300",
|
|
1735
|
+
user_identity: "Free",
|
|
1736
|
+
is_freshman: "0",
|
|
1737
|
+
scope: "marscode",
|
|
1738
|
+
tenant: "marscode",
|
|
1739
|
+
region: "CN",
|
|
1740
|
+
aiRegion: "CN",
|
|
1741
|
+
solo_chat_mode: "code",
|
|
1742
|
+
is_privacy_mode: 1,
|
|
1743
|
+
privacy_mode: "on",
|
|
1744
|
+
...sessionId === void 0 ? {} : { biz_session_id: sessionId },
|
|
1745
|
+
web_id: webId,
|
|
1746
|
+
biz_user_id: userId,
|
|
1747
|
+
user_unique_id: userId
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
function extractContent(raw) {
|
|
1751
|
+
try {
|
|
1752
|
+
const parsed = JSON.parse(raw);
|
|
1753
|
+
if (Array.isArray(parsed)) return parsed.filter((c) => typeof c === "object" && c !== null && c.type === "text").map((c) => c.text_content ?? "").join("");
|
|
1754
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.messages)) {
|
|
1755
|
+
const parts = [];
|
|
1756
|
+
for (const msg of parsed.messages) {
|
|
1757
|
+
if (typeof msg !== "object" || msg === null) continue;
|
|
1758
|
+
const pi = msg.plan_item;
|
|
1759
|
+
if (typeof pi !== "object" || pi === null) continue;
|
|
1760
|
+
const tc = pi.tool_call_info;
|
|
1761
|
+
if (typeof tc !== "object" || tc === null) continue;
|
|
1762
|
+
const tool = tc;
|
|
1763
|
+
if (tool.name === "finish" && tool.params?.summary) parts.push(tool.params.summary);
|
|
1764
|
+
else if (tool.result?.data?.summary && tool.name !== "finish") parts.push(tool.result.data.summary);
|
|
1765
|
+
}
|
|
1766
|
+
return parts.join("\n\n");
|
|
1767
|
+
}
|
|
1768
|
+
} catch {}
|
|
1769
|
+
return raw;
|
|
1770
|
+
}
|
|
1771
|
+
var TraeSoloRemoteClient = class {
|
|
1772
|
+
options;
|
|
1773
|
+
fetchImpl;
|
|
1774
|
+
baseUrl;
|
|
1775
|
+
pollIntervalMs;
|
|
1776
|
+
timeoutMs;
|
|
1777
|
+
constructor(options) {
|
|
1778
|
+
this.options = options;
|
|
1779
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1780
|
+
this.baseUrl = options.baseUrl ?? "https://solo.trae.cn/api/remote/v1";
|
|
1781
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2e3;
|
|
1782
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
1783
|
+
}
|
|
1784
|
+
async headers() {
|
|
1785
|
+
return {
|
|
1786
|
+
"Authorization": `Cloud-IDE-JWT ${(await this.options.credential()).accessToken}`,
|
|
1787
|
+
"Content-Type": "application/json",
|
|
1788
|
+
"x-trae-client-type": "web",
|
|
1789
|
+
"x-trae-user-timezone": "Asia/Shanghai",
|
|
1790
|
+
"x-preferenced-language": "zh-cn",
|
|
1791
|
+
"Referer": "https://solo.trae.cn/",
|
|
1792
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async chat(messages, model, signal) {
|
|
1796
|
+
const headers = await this.headers();
|
|
1797
|
+
const token = (await this.options.credential()).accessToken;
|
|
1798
|
+
const userId = decodeUserId(token);
|
|
1799
|
+
const webId = generateWebId();
|
|
1800
|
+
const modelInfo = TRAE_SOLO_REMOTE_MODELS.find((m) => m.name.toLowerCase() === model.toLowerCase()) ?? {
|
|
1801
|
+
name: model,
|
|
1802
|
+
displayName: model,
|
|
1803
|
+
multimodal: false
|
|
1804
|
+
};
|
|
1805
|
+
const conversation = serializeTraeConversation(messages);
|
|
1806
|
+
const body = {
|
|
1807
|
+
mode: "code",
|
|
1808
|
+
environment_id: "default",
|
|
1809
|
+
env: "remote",
|
|
1810
|
+
auto_create_project: false,
|
|
1811
|
+
origin: "web",
|
|
1812
|
+
initial_message: {
|
|
1813
|
+
chat_session_id: "",
|
|
1814
|
+
content: [],
|
|
1815
|
+
query: JSON.stringify([{
|
|
1816
|
+
type: "text",
|
|
1817
|
+
data: { content: conversation }
|
|
1818
|
+
}]),
|
|
1819
|
+
model_name: modelInfo.name,
|
|
1820
|
+
agent_type: "solo_agent_remote",
|
|
1821
|
+
model_selection_strategy: "manual",
|
|
1822
|
+
custom_model: {
|
|
1823
|
+
name: modelInfo.name,
|
|
1824
|
+
multimodal: modelInfo.multimodal,
|
|
1825
|
+
is_default: false,
|
|
1826
|
+
display_name: modelInfo.displayName,
|
|
1827
|
+
config_name: modelInfo.name,
|
|
1828
|
+
config_source: 1,
|
|
1829
|
+
provider: "",
|
|
1830
|
+
ak: "",
|
|
1831
|
+
sk: "",
|
|
1832
|
+
base_url: "",
|
|
1833
|
+
auth_type: 0,
|
|
1834
|
+
use_remote_service: true
|
|
1835
|
+
},
|
|
1836
|
+
common_params: buildCommonParams(userId, webId)
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
const createResponse = await this.fetchImpl(`${this.baseUrl}/chat_sessions`, {
|
|
1840
|
+
method: "POST",
|
|
1841
|
+
headers,
|
|
1842
|
+
body: JSON.stringify(body),
|
|
1843
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1844
|
+
});
|
|
1845
|
+
const createJson = await createResponse.json();
|
|
1846
|
+
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"}`);
|
|
1847
|
+
const sessionId = createJson.data.chat_session_id;
|
|
1848
|
+
const start = Date.now();
|
|
1849
|
+
while (Date.now() - start < this.timeoutMs) {
|
|
1850
|
+
if (signal?.aborted) throw new Error("SOLO remote polling aborted");
|
|
1851
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1852
|
+
const messageResponse = await this.fetchImpl(`${this.baseUrl}/chat_sessions/${sessionId}/messages?page_size=50`, {
|
|
1853
|
+
headers,
|
|
1854
|
+
...signal === void 0 ? {} : { signal }
|
|
1855
|
+
});
|
|
1856
|
+
if (!messageResponse.ok) continue;
|
|
1857
|
+
const assistant = ((await messageResponse.json()).data?.items ?? []).find((item) => item.role === "assistant");
|
|
1858
|
+
if (assistant && assistant.status !== "in_progress" && assistant.status !== "failed") return {
|
|
1859
|
+
content: extractContent(assistant.content ?? ""),
|
|
1860
|
+
sessionId
|
|
1861
|
+
};
|
|
1862
|
+
if (assistant?.status === "failed") throw new Error("SOLO remote assistant message failed");
|
|
1863
|
+
}
|
|
1864
|
+
throw new Error("SOLO remote polling timed out");
|
|
1865
|
+
}
|
|
1866
|
+
async fetchModels(signal) {
|
|
1867
|
+
const headers = await this.headers();
|
|
1868
|
+
const response = await this.fetchImpl(`${this.baseUrl}/models?functions=solo_agent_remote,solo_work_remote`, {
|
|
1869
|
+
headers,
|
|
1870
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1871
|
+
});
|
|
1872
|
+
if (!response.ok) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
1873
|
+
const groups = (await response.json()).data?.list ?? [];
|
|
1874
|
+
const preferred = groups.find((group) => group.function === "solo_agent_remote") ?? groups[0];
|
|
1875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1876
|
+
const models = [];
|
|
1877
|
+
for (const raw of preferred?.models ?? []) {
|
|
1878
|
+
const model = parseTraeRemoteModel(raw);
|
|
1879
|
+
if (model === void 0 || seen.has(model.id)) continue;
|
|
1880
|
+
seen.add(model.id);
|
|
1881
|
+
models.push(model);
|
|
1882
|
+
}
|
|
1883
|
+
return models;
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
//#endregion
|
|
1887
|
+
//#region src/raw-chat.ts
|
|
1888
|
+
const TRAE_RAW_CHAT_V2_PATH = "/api/ide/v2/llm_raw_chat";
|
|
1889
|
+
const TRAE_RAW_CHAT_V1_PATH = "/api/ide/v1/llm_raw_chat";
|
|
1890
|
+
/**
|
|
1891
|
+
* Evidence-bounded OpenAI-like draft inferred from Trae's LLMRaw* DTO symbols.
|
|
1892
|
+
* Pure/offline only: the upstream remains disabled until one controlled probe.
|
|
1893
|
+
*/
|
|
1894
|
+
function buildTraeRawChatDraft(input) {
|
|
1895
|
+
if (input.model.trim() === "") throw new Error("Trae raw chat requires a model");
|
|
1896
|
+
if (input.messages.length === 0) throw new Error("Trae raw chat requires messages");
|
|
1897
|
+
return {
|
|
1898
|
+
model: input.model,
|
|
1899
|
+
messages: structuredClone([...input.messages]),
|
|
1900
|
+
stream: true,
|
|
1901
|
+
...input.tools === void 0 || input.tools.length === 0 ? {} : { tools: structuredClone([...input.tools]) },
|
|
1902
|
+
...input.maxTokens === void 0 ? {} : { max_tokens: input.maxTokens },
|
|
1903
|
+
...input.temperature === void 0 ? {} : { temperature: input.temperature },
|
|
1904
|
+
...input.reasoningEffort === void 0 ? {} : { reasoning_effort: input.reasoningEffort },
|
|
1905
|
+
...input.extraInfo === void 0 ? {} : { extra_info: structuredClone(input.extraInfo) }
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
/** Decode OpenAI-like raw-chat chunks while preserving unknown structures. */
|
|
1909
|
+
function decodeRawChatChunk(value) {
|
|
1910
|
+
if (value === "[DONE]") return [{
|
|
1911
|
+
type: "done",
|
|
1912
|
+
finishReason: "stop"
|
|
1913
|
+
}];
|
|
1914
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return [{
|
|
1915
|
+
type: "unknown",
|
|
1916
|
+
value
|
|
1917
|
+
}];
|
|
1918
|
+
const record = value;
|
|
1919
|
+
const result = [];
|
|
1920
|
+
const choices = Array.isArray(record["choices"]) ? record["choices"] : [];
|
|
1921
|
+
for (const rawChoice of choices) {
|
|
1922
|
+
if (typeof rawChoice !== "object" || rawChoice === null) continue;
|
|
1923
|
+
const choice = rawChoice;
|
|
1924
|
+
const delta = typeof choice["delta"] === "object" && choice["delta"] !== null ? choice["delta"] : {};
|
|
1925
|
+
if (typeof delta["reasoning_content"] === "string" && delta["reasoning_content"] !== "") result.push({
|
|
1926
|
+
type: "reasoning",
|
|
1927
|
+
text: delta["reasoning_content"]
|
|
1928
|
+
});
|
|
1929
|
+
if (typeof delta["content"] === "string" && delta["content"] !== "") result.push({
|
|
1930
|
+
type: "text",
|
|
1931
|
+
text: delta["content"]
|
|
1932
|
+
});
|
|
1933
|
+
if (Array.isArray(delta["tool_calls"])) for (const rawTool of delta["tool_calls"]) {
|
|
1934
|
+
if (typeof rawTool !== "object" || rawTool === null) continue;
|
|
1935
|
+
const tool = rawTool;
|
|
1936
|
+
const fn = typeof tool["function"] === "object" && tool["function"] !== null ? tool["function"] : {};
|
|
1937
|
+
result.push({
|
|
1938
|
+
type: "tool-call",
|
|
1939
|
+
index: typeof tool["index"] === "number" ? tool["index"] : 0,
|
|
1940
|
+
...typeof tool["id"] === "string" ? { id: tool["id"] } : {},
|
|
1941
|
+
...typeof fn["name"] === "string" ? { name: fn["name"] } : {},
|
|
1942
|
+
...typeof fn["arguments"] === "string" ? { arguments: fn["arguments"] } : {}
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
if (typeof choice["finish_reason"] === "string" && choice["finish_reason"] !== "") result.push({
|
|
1946
|
+
type: "done",
|
|
1947
|
+
finishReason: choice["finish_reason"]
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
if (typeof record["usage"] === "object" && record["usage"] !== null) {
|
|
1951
|
+
const usage = record["usage"];
|
|
1952
|
+
result.push({
|
|
1953
|
+
type: "usage",
|
|
1954
|
+
...typeof usage["prompt_tokens"] === "number" ? { inputTokens: usage["prompt_tokens"] } : {},
|
|
1955
|
+
...typeof usage["completion_tokens"] === "number" ? { outputTokens: usage["completion_tokens"] } : {},
|
|
1956
|
+
...typeof usage["total_tokens"] === "number" ? { totalTokens: usage["total_tokens"] } : {}
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
return result.length === 0 ? [{
|
|
1960
|
+
type: "unknown",
|
|
1961
|
+
value
|
|
1962
|
+
}] : result;
|
|
1963
|
+
}
|
|
1964
|
+
//#endregion
|
|
1965
|
+
//#region src/raw-runtime-config.ts
|
|
1966
|
+
/** Merge safe current-cache facts with observed behavior flags; no network credentials enter this object. */
|
|
1967
|
+
function traeRawChatExtraInfo(config) {
|
|
1968
|
+
return {
|
|
1969
|
+
...config.nativeFunctionCall === void 0 ? {} : { native_function_call: config.nativeFunctionCall },
|
|
1970
|
+
...config.useV2Process === void 0 ? {} : { use_v2_process: config.useV2Process },
|
|
1971
|
+
...config.maxModeEnabled === void 0 ? {} : { v2_max_mode_enabled: config.maxModeEnabled },
|
|
1972
|
+
...config.maxToolcallChars === void 0 ? {} : { v3_max_toolcall_chars: config.maxToolcallChars },
|
|
1973
|
+
...config.streamThrottleEnabled === void 0 ? {} : { v3_stream_throttle_enabled: config.streamThrottleEnabled }
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
function buildTraeRawChatRuntimeConfig(modelName, cached, behavior = {}) {
|
|
1977
|
+
return {
|
|
1978
|
+
configName: modelName,
|
|
1979
|
+
modelName,
|
|
1980
|
+
...cached?.promptMaxTokens === void 0 ? {} : { promptMaxTokens: cached.promptMaxTokens },
|
|
1981
|
+
...cached?.maxTokens === void 0 ? {} : { maxTokens: cached.maxTokens },
|
|
1982
|
+
...cached?.maxTurn === void 0 ? {} : { maxTurn: cached.maxTurn },
|
|
1983
|
+
...cached?.multimodal === void 0 ? {} : { multimodal: cached.multimodal },
|
|
1984
|
+
...cached?.customConfig === void 0 ? {} : { customConfig: structuredClone(cached.customConfig) },
|
|
1985
|
+
...behavior.nativeFunctionCall === void 0 ? {} : { nativeFunctionCall: behavior.nativeFunctionCall },
|
|
1986
|
+
...behavior.passBackReasoning === void 0 ? {} : { passBackReasoning: behavior.passBackReasoning },
|
|
1987
|
+
...behavior.useV2Process === void 0 ? {} : { useV2Process: behavior.useV2Process },
|
|
1988
|
+
...behavior.maxModeEnabled === void 0 ? {} : { maxModeEnabled: behavior.maxModeEnabled },
|
|
1989
|
+
...behavior.maxToolcallChars === void 0 ? {} : { maxToolcallChars: behavior.maxToolcallChars },
|
|
1990
|
+
...behavior.streamThrottleEnabled === void 0 ? {} : { streamThrottleEnabled: behavior.streamThrottleEnabled }
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
//#endregion
|
|
1994
|
+
//#region src/raw-upstream.ts
|
|
1995
|
+
function classifyTraeRawChatFailure(message) {
|
|
1996
|
+
const text = message.trim();
|
|
1997
|
+
if (text === "") return "empty";
|
|
1998
|
+
if (text.startsWith("{") || text.startsWith("[")) return "json";
|
|
1999
|
+
if (/permission|forbidden|unauthorized|auth/i.test(text)) return "permission";
|
|
2000
|
+
if (/model|config_name|config name/i.test(text)) return "model";
|
|
2001
|
+
if (/schema|field|parameter|invalid|missing|required|parse/i.test(text)) return "schema";
|
|
2002
|
+
if (/^Trae Raw Chat returned HTTP \d{3}$/i.test(text)) return "generic-http";
|
|
2003
|
+
return "other";
|
|
2004
|
+
}
|
|
2005
|
+
function errorKind(status) {
|
|
2006
|
+
if (status === 401 || status === 403) return "authentication";
|
|
2007
|
+
if (status === 402) return "hard_credit";
|
|
2008
|
+
if (status === 429) return "soft_rate";
|
|
2009
|
+
if (status === 404) return "not_found";
|
|
2010
|
+
if (status >= 500) return "server";
|
|
2011
|
+
return "client";
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Evidence-gated Raw Chat client. It is fully testable through injected fetch,
|
|
2015
|
+
* but production callers must not construct it without verified config values.
|
|
2016
|
+
*/
|
|
2017
|
+
var TraeRawChatUpstreamClient = class {
|
|
2018
|
+
options;
|
|
2019
|
+
fetchImpl;
|
|
2020
|
+
constructor(options) {
|
|
2021
|
+
this.options = options;
|
|
2022
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2023
|
+
if (options.config.configName.trim() === "") throw new Error("Trae Raw Chat configName must be evidence-backed and non-empty");
|
|
2024
|
+
}
|
|
2025
|
+
async chatStream(bodyJson, signal) {
|
|
2026
|
+
let input;
|
|
2027
|
+
try {
|
|
2028
|
+
input = JSON.parse(bodyJson);
|
|
2029
|
+
} catch {
|
|
2030
|
+
return {
|
|
2031
|
+
ok: false,
|
|
2032
|
+
status: 400,
|
|
2033
|
+
kind: "client",
|
|
2034
|
+
message: "invalid JSON request"
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
if (!Array.isArray(input.messages) || input.messages.length === 0) return {
|
|
2038
|
+
ok: false,
|
|
2039
|
+
status: 400,
|
|
2040
|
+
kind: "client",
|
|
2041
|
+
message: "messages are required"
|
|
2042
|
+
};
|
|
2043
|
+
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
2044
|
+
const headers = buildTraeCnHeaders(credential, identity, {
|
|
2045
|
+
requestId: crypto.randomUUID(),
|
|
2046
|
+
profile: "raw-chat"
|
|
2047
|
+
});
|
|
2048
|
+
const runtime = this.options.config.runtime;
|
|
2049
|
+
const runtimeExtra = runtime === void 0 ? {} : traeRawChatExtraInfo(runtime);
|
|
2050
|
+
const body = {
|
|
2051
|
+
...buildTraeRawChatDraft({
|
|
2052
|
+
model: this.options.config.model,
|
|
2053
|
+
messages: input.messages,
|
|
2054
|
+
...input.tools === void 0 ? {} : { tools: input.tools },
|
|
2055
|
+
...input.max_tokens === void 0 ? {} : { maxTokens: input.max_tokens },
|
|
2056
|
+
...input.temperature === void 0 ? {} : { temperature: input.temperature },
|
|
2057
|
+
...input.reasoning_effort === void 0 ? {} : { reasoningEffort: input.reasoning_effort },
|
|
2058
|
+
...Object.keys(runtimeExtra).length === 0 && this.options.config.extraInfo === void 0 ? {} : { extraInfo: {
|
|
2059
|
+
...runtimeExtra,
|
|
2060
|
+
...this.options.config.extraInfo
|
|
2061
|
+
} }
|
|
2062
|
+
}),
|
|
2063
|
+
config_name: this.options.config.configName,
|
|
2064
|
+
...this.options.config.promptSet === void 0 ? {} : { prompt_set: this.options.config.promptSet },
|
|
2065
|
+
...this.options.config.abVersion === void 0 ? {} : { ab_version: this.options.config.abVersion },
|
|
2066
|
+
pass_back_reasoning: runtime?.passBackReasoning ?? this.options.config.passBackReasoning
|
|
2067
|
+
};
|
|
2068
|
+
let response;
|
|
2069
|
+
try {
|
|
2070
|
+
response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.host, TRAE_RAW_CHAT_V2_PATH), {
|
|
2071
|
+
method: "POST",
|
|
2072
|
+
headers,
|
|
2073
|
+
body: JSON.stringify(body),
|
|
2074
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
2075
|
+
});
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
return {
|
|
2078
|
+
ok: false,
|
|
2079
|
+
status: 0,
|
|
2080
|
+
kind: "server",
|
|
2081
|
+
message: `transport error: ${String(error)}`
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
if (response.ok) return {
|
|
2085
|
+
ok: true,
|
|
2086
|
+
response
|
|
2087
|
+
};
|
|
2088
|
+
const text = (await response.text()).slice(0, 1024);
|
|
2089
|
+
return {
|
|
2090
|
+
ok: false,
|
|
2091
|
+
status: response.status,
|
|
2092
|
+
kind: errorKind(response.status),
|
|
2093
|
+
message: text || `Trae Raw Chat returned HTTP ${response.status}`
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/raw-capability.ts
|
|
2099
|
+
/** One short non-tool request decides whether Raw Chat may become primary. */
|
|
2100
|
+
async function probeTraeRawChatCapability(client, signal) {
|
|
2101
|
+
const body = JSON.stringify({
|
|
2102
|
+
model: "qwen-3.7-plus",
|
|
2103
|
+
messages: [{
|
|
2104
|
+
role: "user",
|
|
2105
|
+
content: "Reply with exactly: OK"
|
|
2106
|
+
}],
|
|
2107
|
+
max_tokens: 8,
|
|
2108
|
+
temperature: 0
|
|
2109
|
+
});
|
|
2110
|
+
const result = await client.chatStream(body, signal);
|
|
2111
|
+
if (result.ok) {
|
|
2112
|
+
const contentType = result.response.headers.get("content-type");
|
|
2113
|
+
await result.response.body?.cancel().catch(() => {});
|
|
2114
|
+
return {
|
|
2115
|
+
available: true,
|
|
2116
|
+
contentType
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
available: false,
|
|
2121
|
+
reason: result.kind === "authentication" ? "authentication" : result.kind === "hard_credit" ? "credit" : result.kind === "soft_rate" ? "rate" : result.status === 400 || result.status === 404 || result.status === 415 || result.kind === "unconfigured" ? "protocol" : result.kind === "server" && result.status === 0 ? "transport" : "server",
|
|
2122
|
+
status: result.status
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
//#endregion
|
|
2126
|
+
//#region src/raw-capability-state.ts
|
|
2127
|
+
/** In-memory capability state; success/failure expires and config changes invalidate it. */
|
|
2128
|
+
var TraeRawCapabilityState = class {
|
|
2129
|
+
ttlMs;
|
|
2130
|
+
snapshot = {};
|
|
2131
|
+
constructor(ttlMs = 18e5) {
|
|
2132
|
+
this.ttlMs = ttlMs;
|
|
2133
|
+
}
|
|
2134
|
+
current(fingerprint, now = Date.now()) {
|
|
2135
|
+
const value = this.snapshot;
|
|
2136
|
+
if (value.capability === void 0 || value.checkedAtMs === void 0 || value.fingerprint !== fingerprint) return void 0;
|
|
2137
|
+
return now - value.checkedAtMs <= this.ttlMs ? value.capability : void 0;
|
|
2138
|
+
}
|
|
2139
|
+
record(fingerprint, capability, now = Date.now()) {
|
|
2140
|
+
this.snapshot = {
|
|
2141
|
+
fingerprint,
|
|
2142
|
+
capability,
|
|
2143
|
+
checkedAtMs: now
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
inspect() {
|
|
2147
|
+
return { ...this.snapshot };
|
|
2148
|
+
}
|
|
2149
|
+
invalidate() {
|
|
2150
|
+
this.snapshot = {};
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
//#endregion
|
|
2154
|
+
//#region src/raw-capability-controller.ts
|
|
2155
|
+
/** Explicitly-triggered, single-flight Raw Chat capability checks. */
|
|
2156
|
+
var TraeRawCapabilityController = class {
|
|
2157
|
+
options;
|
|
2158
|
+
state;
|
|
2159
|
+
enabled;
|
|
2160
|
+
inflight;
|
|
2161
|
+
constructor(options) {
|
|
2162
|
+
this.options = options;
|
|
2163
|
+
this.state = options.state ?? new TraeRawCapabilityState();
|
|
2164
|
+
this.enabled = options.enabled ?? false;
|
|
2165
|
+
}
|
|
2166
|
+
current(fingerprint) {
|
|
2167
|
+
return this.enabled ? this.state.current(fingerprint) : void 0;
|
|
2168
|
+
}
|
|
2169
|
+
setEnabled(enabled) {
|
|
2170
|
+
if (this.enabled !== enabled) this.state.invalidate();
|
|
2171
|
+
this.enabled = enabled;
|
|
2172
|
+
}
|
|
2173
|
+
inspect() {
|
|
2174
|
+
return this.state.inspect();
|
|
2175
|
+
}
|
|
2176
|
+
isEnabled() {
|
|
2177
|
+
return this.enabled;
|
|
2178
|
+
}
|
|
2179
|
+
invalidate() {
|
|
2180
|
+
this.state.invalidate();
|
|
2181
|
+
}
|
|
2182
|
+
probe(fingerprint, signal) {
|
|
2183
|
+
if (!this.enabled) return Promise.resolve({
|
|
2184
|
+
available: false,
|
|
2185
|
+
reason: "protocol",
|
|
2186
|
+
status: 0
|
|
2187
|
+
});
|
|
2188
|
+
const cached = this.state.current(fingerprint);
|
|
2189
|
+
if (cached !== void 0) return Promise.resolve(cached);
|
|
2190
|
+
this.inflight ??= probeTraeRawChatCapability(this.options.client, signal).then((result) => {
|
|
2191
|
+
this.state.record(fingerprint, result);
|
|
2192
|
+
return result;
|
|
2193
|
+
}).finally(() => {
|
|
2194
|
+
this.inflight = void 0;
|
|
2195
|
+
});
|
|
2196
|
+
return this.inflight;
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
//#endregion
|
|
2200
|
+
//#region src/raw-fingerprint.ts
|
|
2201
|
+
/** Stable non-secret fingerprint for capability-cache invalidation. */
|
|
2202
|
+
function rawCapabilityFingerprint(input) {
|
|
2203
|
+
const stable = JSON.stringify({
|
|
2204
|
+
endpoint: input.endpoint,
|
|
2205
|
+
edition: input.edition,
|
|
2206
|
+
appVersion: input.identity.appVersion ?? "",
|
|
2207
|
+
buildVersion: input.identity.buildVersion ?? "",
|
|
2208
|
+
runtime: input.runtime
|
|
2209
|
+
});
|
|
2210
|
+
return createHash("sha256").update(stable).digest("hex");
|
|
2211
|
+
}
|
|
2212
|
+
//#endregion
|
|
2213
|
+
//#region src/raw-diagnostic.ts
|
|
2214
|
+
/** Project capability facts to a redacted, user-safe status. */
|
|
2215
|
+
function rawCapabilityDiagnostic(enabled, capability, checkedAtMs) {
|
|
2216
|
+
if (!enabled) return { state: "disabled" };
|
|
2217
|
+
if (capability === void 0) return { state: "unchecked" };
|
|
2218
|
+
if (capability.available) return {
|
|
2219
|
+
state: "available",
|
|
2220
|
+
...checkedAtMs === void 0 ? {} : { checkedAtMs }
|
|
2221
|
+
};
|
|
2222
|
+
return {
|
|
2223
|
+
state: capability.reason === "protocol" ? "protocol-gated" : capability.reason,
|
|
2224
|
+
status: capability.status,
|
|
2225
|
+
...checkedAtMs === void 0 ? {} : { checkedAtMs }
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/fallback-upstream.ts
|
|
2230
|
+
/**
|
|
2231
|
+
* Raw Chat first, SOLO Remote second. Fallback is intentionally narrow:
|
|
2232
|
+
* only definitive pre-stream schema/route incompatibilities may retry the same
|
|
2233
|
+
* user request. Authentication, credit, rate, cancellation, transport/server
|
|
2234
|
+
* failures, and every successful Response are never replayed.
|
|
2235
|
+
*/
|
|
2236
|
+
var TraeFallbackUpstreamClient = class {
|
|
2237
|
+
options;
|
|
2238
|
+
constructor(options) {
|
|
2239
|
+
this.options = options;
|
|
2240
|
+
}
|
|
2241
|
+
async chatStream(bodyJson, signal) {
|
|
2242
|
+
const primary = await this.options.primary.chatStream(bodyJson, signal);
|
|
2243
|
+
if (primary.ok) return primary;
|
|
2244
|
+
if (!this.canFallback(primary, signal)) return primary;
|
|
2245
|
+
this.options.onFallback?.(primary);
|
|
2246
|
+
return this.options.fallback.chatStream(bodyJson, signal);
|
|
2247
|
+
}
|
|
2248
|
+
canFallback(result, signal) {
|
|
2249
|
+
if (signal?.aborted === true) return false;
|
|
2250
|
+
if (result.kind === "authentication" || result.kind === "hard_credit" || result.kind === "soft_rate") return false;
|
|
2251
|
+
return result.status === 400 || result.status === 404 || result.status === 415 || result.kind === "unconfigured";
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
//#endregion
|
|
2255
|
+
//#region src/gated-upstream.ts
|
|
2256
|
+
/** Select Raw Chat only after a successful capability probe; SOLO is the safe default. */
|
|
2257
|
+
var TraeGatedUpstreamClient = class {
|
|
2258
|
+
options;
|
|
2259
|
+
constructor(options) {
|
|
2260
|
+
this.options = options;
|
|
2261
|
+
}
|
|
2262
|
+
chatStream(bodyJson, signal) {
|
|
2263
|
+
if (this.options.capability()?.available !== true) return this.options.solo.chatStream(bodyJson, signal);
|
|
2264
|
+
return new TraeFallbackUpstreamClient({
|
|
2265
|
+
primary: this.options.raw,
|
|
2266
|
+
fallback: this.options.solo,
|
|
2267
|
+
...this.options.onFallback === void 0 ? {} : { onFallback: this.options.onFallback }
|
|
2268
|
+
}).chatStream(bodyJson, signal);
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
//#endregion
|
|
2272
|
+
//#region src/raw-gateway.ts
|
|
2273
|
+
/** Assemble the opt-in capability state and safe Raw→SOLO routing. */
|
|
2274
|
+
function createTraeRawGateway(options) {
|
|
2275
|
+
const fingerprint = () => rawCapabilityFingerprint(options);
|
|
2276
|
+
const controller = new TraeRawCapabilityController({
|
|
2277
|
+
client: options.raw,
|
|
2278
|
+
enabled: options.enabled ?? false
|
|
2279
|
+
});
|
|
2280
|
+
return {
|
|
2281
|
+
upstream: new TraeGatedUpstreamClient({
|
|
2282
|
+
raw: options.raw,
|
|
2283
|
+
solo: options.solo,
|
|
2284
|
+
capability: () => controller.current(fingerprint())
|
|
2285
|
+
}),
|
|
2286
|
+
probe: (signal) => controller.probe(fingerprint(), signal),
|
|
2287
|
+
diagnostic: () => {
|
|
2288
|
+
const snapshot = controller.inspect();
|
|
2289
|
+
return rawCapabilityDiagnostic(controller.isEnabled(), snapshot.capability, snapshot.checkedAtMs);
|
|
2290
|
+
},
|
|
2291
|
+
invalidate: () => controller.invalidate()
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
//#endregion
|
|
2295
|
+
//#region src/model-cache.ts
|
|
2296
|
+
const execFileAsync = promisify(execFile);
|
|
2297
|
+
function positive$1(value) {
|
|
2298
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2299
|
+
}
|
|
2300
|
+
/** Parse a safe subset of one cached model entry; credentials and endpoints are intentionally omitted. */
|
|
2301
|
+
function parseTraeCachedModel(value) {
|
|
2302
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2303
|
+
const raw = value;
|
|
2304
|
+
if (typeof raw["name"] !== "string" || raw["name"] === "") return void 0;
|
|
2305
|
+
let customConfig;
|
|
2306
|
+
if (typeof raw["custom_config"] === "string" && raw["custom_config"] !== "") try {
|
|
2307
|
+
const parsed = JSON.parse(raw["custom_config"]);
|
|
2308
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) customConfig = parsed;
|
|
2309
|
+
} catch {}
|
|
2310
|
+
const promptMaxTokens = positive$1(raw["prompt_max_tokens"]);
|
|
2311
|
+
const maxTokens = positive$1(raw["max_tokens"]);
|
|
2312
|
+
const maxTurn = positive$1(raw["max_turn"]);
|
|
2313
|
+
return {
|
|
2314
|
+
name: raw["name"],
|
|
2315
|
+
...customConfig === void 0 ? {} : { customConfig },
|
|
2316
|
+
...promptMaxTokens === void 0 ? {} : { promptMaxTokens },
|
|
2317
|
+
...maxTokens === void 0 ? {} : { maxTokens },
|
|
2318
|
+
...maxTurn === void 0 ? {} : { maxTurn },
|
|
2319
|
+
...typeof raw["multimodal"] === "boolean" ? { multimodal: raw["multimodal"] } : {},
|
|
2320
|
+
...typeof raw["model_type"] === "string" ? { modelType: raw["model_type"] } : {}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
/** Read Trae's own current user's model map via sqlite3 without exposing secrets. */
|
|
2324
|
+
async function readTraeCachedModel(functionName, modelName, userId) {
|
|
2325
|
+
const database = join(homedir(), "Library", "Application Support", "Trae CN", "User", "globalStorage", "state.vscdb");
|
|
2326
|
+
const key = `${userId}_AI.agent.model.model_list_map`;
|
|
2327
|
+
const sql = `select value from ItemTable where key=${JSON.stringify(key)} limit 1;`;
|
|
2328
|
+
const { stdout } = await execFileAsync("sqlite3", [database, sql], { maxBuffer: 8388608 });
|
|
2329
|
+
const document = JSON.parse(stdout);
|
|
2330
|
+
return parseTraeCachedModel((Array.isArray(document[functionName]) ? document[functionName] : []).find((item) => typeof item === "object" && item !== null && item.name === modelName));
|
|
2331
|
+
}
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/raw-resolver.ts
|
|
2334
|
+
/** Resolve current desktop identity and safe cached model config for capability probing. */
|
|
2335
|
+
async function resolveTraeRawRuntime(store, modelName) {
|
|
2336
|
+
const credential = await store.resolve();
|
|
2337
|
+
const candidate = store.candidates().find((item) => item.edition === credential.edition);
|
|
2338
|
+
if (candidate === void 0) throw new Error(`no Trae ${credential.edition} storage candidate for Raw Chat identity`);
|
|
2339
|
+
const identity = await readTraeIdentity(candidate);
|
|
2340
|
+
const cached = credential.edition === "cn" ? await readTraeCachedModel("solo_agent", modelName, credential.userId).catch(() => void 0) : void 0;
|
|
2341
|
+
return {
|
|
2342
|
+
identity,
|
|
2343
|
+
runtime: buildTraeRawChatRuntimeConfig(modelName, cached, {
|
|
2344
|
+
passBackReasoning: true,
|
|
2345
|
+
nativeFunctionCall: cached?.customConfig?.["native_function_call"] === true,
|
|
2346
|
+
useV2Process: cached?.customConfig?.["use_v2_process"] === true
|
|
2347
|
+
})
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
//#endregion
|
|
2351
|
+
//#region src/delegating-upstream.ts
|
|
2352
|
+
/** Stable shim dependency whose delegate can be replaced after async setup. */
|
|
2353
|
+
var TraeDelegatingUpstreamClient = class {
|
|
2354
|
+
delegate;
|
|
2355
|
+
constructor(delegate) {
|
|
2356
|
+
this.delegate = delegate;
|
|
2357
|
+
}
|
|
2358
|
+
replace(delegate) {
|
|
2359
|
+
this.delegate = delegate;
|
|
2360
|
+
}
|
|
2361
|
+
chatStream(bodyJson, signal) {
|
|
2362
|
+
return this.delegate.chatStream(bodyJson, signal);
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
//#endregion
|
|
2366
|
+
//#region src/usage.ts
|
|
2367
|
+
/**
|
|
2368
|
+
* Read-only Trae usage/credits client.
|
|
2369
|
+
*
|
|
2370
|
+
* Sources the verified `api.trae.cn` pay/ug endpoints (see
|
|
2371
|
+
* `docs/USAGE_API_RESEARCH.md`). All queries are read-only and do not consume
|
|
2372
|
+
* Trae credits. The per-session consumption detail table is deliberately not
|
|
2373
|
+
* included: the endpoint returns no rows for the current account, so we only
|
|
2374
|
+
* expose what is actually retrievable.
|
|
2375
|
+
*/
|
|
2376
|
+
const TRAE_PAY_BASE = "https://api.trae.cn";
|
|
2377
|
+
function asNumber(value) {
|
|
2378
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2379
|
+
}
|
|
2380
|
+
function parseUsageSnapshot(payload) {
|
|
2381
|
+
const summaryRaw = payload["usage_summary"];
|
|
2382
|
+
const summary = {
|
|
2383
|
+
totalAmount: asNumber(summaryRaw?.["total_amount"]) ?? 0,
|
|
2384
|
+
consumedAmount: asNumber(summaryRaw?.["consumed_amount"]) ?? 0,
|
|
2385
|
+
consumptionRatio: asNumber(summaryRaw?.["consumption_ratio"]) ?? 0
|
|
2386
|
+
};
|
|
2387
|
+
const trial = payload["trial_status"];
|
|
2388
|
+
const packs = [];
|
|
2389
|
+
const rawPacks = Array.isArray(payload["user_entitlement_pack_list"]) ? payload["user_entitlement_pack_list"] : [];
|
|
2390
|
+
for (const raw of rawPacks) {
|
|
2391
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
2392
|
+
const pack = raw;
|
|
2393
|
+
const base = pack["entitlement_base_info"];
|
|
2394
|
+
const quota = base?.["quota"];
|
|
2395
|
+
const usage = pack["usage"];
|
|
2396
|
+
const packageQuota = ((base?.["product_extra"])?.["package_extra"])?.["quota"];
|
|
2397
|
+
const creditsLimit = asNumber(packageQuota?.["credits_limit"]) ?? asNumber(quota?.["credits_limit"]);
|
|
2398
|
+
const consumedCredits = asNumber(usage?.["credits_amount"]);
|
|
2399
|
+
const availableEndpoint = asNumber(base?.["available_endpoint"]);
|
|
2400
|
+
packs.push({
|
|
2401
|
+
displayDesc: typeof pack["display_desc"] === "string" ? pack["display_desc"] : "",
|
|
2402
|
+
entitlementId: typeof base?.["entitlement_id"] === "string" ? base["entitlement_id"] : "",
|
|
2403
|
+
endTimeMs: asNumber(base?.["end_time"]) ?? 0,
|
|
2404
|
+
currency: asNumber(base?.["currency"]) ?? 0,
|
|
2405
|
+
...availableEndpoint === void 0 ? {} : { availableEndpoint },
|
|
2406
|
+
...creditsLimit === void 0 ? {} : { creditsLimit },
|
|
2407
|
+
...consumedCredits === void 0 ? {} : { consumedCredits }
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
return {
|
|
2411
|
+
isCreditsBilling: payload["is_credits_billing"] === true,
|
|
2412
|
+
isDollarUsageBilling: payload["is_dollar_usage_billing"] === true,
|
|
2413
|
+
isPayFreshman: payload["is_pay_freshman"] === true,
|
|
2414
|
+
inTrial: trial?.["is_in_trial"] === true,
|
|
2415
|
+
trialEndTimeMs: asNumber(trial?.["trial_end_time"]) ?? 0,
|
|
2416
|
+
summary,
|
|
2417
|
+
packs
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* A read-only client for the verified Trae usage/credits endpoints.
|
|
2422
|
+
* All methods are safe to call from a plugin and never mutate account state.
|
|
2423
|
+
*/
|
|
2424
|
+
var TraeUsageClient = class {
|
|
2425
|
+
options;
|
|
2426
|
+
fetchImpl;
|
|
2427
|
+
baseUrl;
|
|
2428
|
+
timeoutMs;
|
|
2429
|
+
constructor(options) {
|
|
2430
|
+
this.options = options;
|
|
2431
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2432
|
+
this.baseUrl = options.baseUrl ?? "https://api.trae.cn";
|
|
2433
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2434
|
+
}
|
|
2435
|
+
async authedHeaders() {
|
|
2436
|
+
const credential = await this.options.credential();
|
|
2437
|
+
if (credential === void 0 || credential.accessToken === "") throw new Error("Trae credential is not available; cannot query usage");
|
|
2438
|
+
return {
|
|
2439
|
+
"Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
|
|
2440
|
+
"Content-Type": "application/json",
|
|
2441
|
+
"User-Agent": "Mozilla/5.0",
|
|
2442
|
+
"Origin": "https://www.trae.cn",
|
|
2443
|
+
"Referer": "https://www.trae.cn/"
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
async post(path, data, signal) {
|
|
2447
|
+
const headers = await this.authedHeaders();
|
|
2448
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
2449
|
+
method: "POST",
|
|
2450
|
+
headers,
|
|
2451
|
+
body: JSON.stringify(data),
|
|
2452
|
+
signal: signal ?? AbortSignal.timeout(this.timeoutMs)
|
|
2453
|
+
});
|
|
2454
|
+
if (!response.ok) throw new Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
|
|
2455
|
+
return await response.json();
|
|
2456
|
+
}
|
|
2457
|
+
/** Total entitlements / credits and per-pack breakdown. */
|
|
2458
|
+
async snapshot(signal) {
|
|
2459
|
+
return parseUsageSnapshot(await this.post("/trae/api/v2/pay/web_user_ent_usage", { require_usage: true }, signal));
|
|
2460
|
+
}
|
|
2461
|
+
/** Daily check-in status. */
|
|
2462
|
+
async checkinStatus(signal) {
|
|
2463
|
+
const payload = await this.post("/trae/api/v2/ug/checkin_credits/status", {}, signal);
|
|
2464
|
+
return {
|
|
2465
|
+
checkedIn: payload["checked_in"] === true,
|
|
2466
|
+
credits: asNumber(payload["credits"]) ?? 0,
|
|
2467
|
+
enabled: payload["enable"] !== false
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
/** Rewards / activity rules. */
|
|
2471
|
+
async activities(signal) {
|
|
2472
|
+
const payload = await this.post("/trae/api/v2/ug/activity/info", {}, signal);
|
|
2473
|
+
const rawActivities = Array.isArray(payload["commercial_activities"]) ? payload["commercial_activities"] : [];
|
|
2474
|
+
const activities = [];
|
|
2475
|
+
for (const raw of rawActivities) {
|
|
2476
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
2477
|
+
const rule = raw;
|
|
2478
|
+
activities.push({
|
|
2479
|
+
activityId: typeof rule["activity_id"] === "string" ? rule["activity_id"] : "",
|
|
2480
|
+
enabled: rule["Enabled"] === true,
|
|
2481
|
+
activityType: asNumber(rule["activity_type"]) ?? 0,
|
|
2482
|
+
startTimeMs: asNumber(rule["start_time_ms"]) ?? 0,
|
|
2483
|
+
endTimeMs: asNumber(rule["end_time_ms"]) ?? 0,
|
|
2484
|
+
...rule["work_extra"] === void 0 ? {} : { workExtra: rule["work_extra"] }
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
return activities;
|
|
2488
|
+
}
|
|
2489
|
+
/** Convenience: snapshot + check-in + activities in one call (best-effort, non-fatal on missing). */
|
|
2490
|
+
async view(signal) {
|
|
2491
|
+
const [snapshot, checkin, activities] = await Promise.all([
|
|
2492
|
+
this.snapshot(signal),
|
|
2493
|
+
this.checkinStatus(signal),
|
|
2494
|
+
this.activities(signal)
|
|
2495
|
+
]);
|
|
2496
|
+
return {
|
|
2497
|
+
snapshot,
|
|
2498
|
+
checkin,
|
|
2499
|
+
activities
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
//#endregion
|
|
2504
|
+
//#region src/status-paths.ts
|
|
2505
|
+
/**
|
|
2506
|
+
* Node-free constants and types shared by the Host and browser halves.
|
|
2507
|
+
* Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
|
|
2508
|
+
* stays consistent with that project's external presentation.
|
|
2509
|
+
*/
|
|
2510
|
+
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
2511
|
+
const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
|
|
2512
|
+
/** Plugin-owned live model refresh endpoint. */
|
|
2513
|
+
const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
|
|
2514
|
+
/** Plugin-owned local account rescan endpoint. */
|
|
2515
|
+
const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
|
|
2516
|
+
//#endregion
|
|
2517
|
+
//#region src/web-status.ts
|
|
2518
|
+
/** Redact token-like content before it crosses to the browser. */
|
|
2519
|
+
function safeMessage(error) {
|
|
2520
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
|
|
2521
|
+
}
|
|
2522
|
+
function json(res, status, body) {
|
|
2523
|
+
const payload = JSON.stringify(body);
|
|
2524
|
+
res.writeHead(status, {
|
|
2525
|
+
"Content-Type": "application/json",
|
|
2526
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
2527
|
+
});
|
|
2528
|
+
res.end(payload);
|
|
2529
|
+
}
|
|
2530
|
+
/** Loopback browser origins only; other devices are refused until trusted origins exist. */
|
|
2531
|
+
function loopbackOrigin(req) {
|
|
2532
|
+
const origin = req.headers.origin;
|
|
2533
|
+
if (origin === void 0) return true;
|
|
2534
|
+
try {
|
|
2535
|
+
const { hostname } = new URL(origin);
|
|
2536
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
2537
|
+
} catch {
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
/** Map the credit snapshot to the card's compact credit document. */
|
|
2542
|
+
function toCredits(snapshot) {
|
|
2543
|
+
const { totalAmount, consumedAmount } = snapshot.summary;
|
|
2544
|
+
const credit = (value) => Math.round(value * 1e4) / 1e4;
|
|
2545
|
+
const accounts = snapshot.packs.map((pack) => ({
|
|
2546
|
+
displayDesc: pack.displayDesc,
|
|
2547
|
+
remain: credit(Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))),
|
|
2548
|
+
size: pack.creditsLimit ?? 0
|
|
2549
|
+
}));
|
|
2550
|
+
const remaining = (endpoint) => snapshot.packs.filter((pack) => pack.availableEndpoint === endpoint).reduce((sum, pack) => credit(sum + Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))), 0);
|
|
2551
|
+
return {
|
|
2552
|
+
total: totalAmount,
|
|
2553
|
+
consumed: consumedAmount,
|
|
2554
|
+
available: totalAmount - consumedAmount,
|
|
2555
|
+
workAvailable: remaining(1),
|
|
2556
|
+
generalAvailable: remaining(0),
|
|
2557
|
+
accounts
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Assemble the card's usage document. Sign-in state is read-only; credit is a
|
|
2562
|
+
* live billing answer whose failure degrades to `creditsError` rather than
|
|
2563
|
+
* failing the whole document.
|
|
2564
|
+
*/
|
|
2565
|
+
async function traeWebUsage(deps) {
|
|
2566
|
+
const accounts = await deps.store.accounts();
|
|
2567
|
+
if ((await deps.store.status()).state !== "signed-in") return {
|
|
2568
|
+
status: "signed-out",
|
|
2569
|
+
accounts
|
|
2570
|
+
};
|
|
2571
|
+
let credential;
|
|
2572
|
+
try {
|
|
2573
|
+
credential = await deps.store.resolve();
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
return {
|
|
2576
|
+
status: "signed-out",
|
|
2577
|
+
accounts,
|
|
2578
|
+
message: safeMessage(error)
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
const account = {
|
|
2582
|
+
accountId: accounts.find((item) => item.selected)?.id ?? "",
|
|
2583
|
+
accountName: credential.accountName ?? credential.userId,
|
|
2584
|
+
tokenExpiresAtMs: credential.expiresAtMs,
|
|
2585
|
+
accounts,
|
|
2586
|
+
models: deps.displayModels().map((model) => ({
|
|
2587
|
+
...model,
|
|
2588
|
+
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
2589
|
+
})),
|
|
2590
|
+
enabledModelIds: [...deps.enabledModelIds()],
|
|
2591
|
+
enabled1mModelIds: [...deps.enabled1mModelIds()],
|
|
2592
|
+
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic() }
|
|
2593
|
+
};
|
|
2594
|
+
try {
|
|
2595
|
+
const snapshot = await deps.client.snapshot();
|
|
2596
|
+
return {
|
|
2597
|
+
status: "signed-in",
|
|
2598
|
+
...account,
|
|
2599
|
+
credits: toCredits(snapshot)
|
|
2600
|
+
};
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
return {
|
|
2603
|
+
status: "signed-in",
|
|
2604
|
+
...account,
|
|
2605
|
+
creditsError: safeMessage(error)
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
/** Mount the GET usage route on an optional webServer context. */
|
|
2610
|
+
function registerTraeUsageRoute(ctx, deps) {
|
|
2611
|
+
ctx.effect(() => {
|
|
2612
|
+
const disposeUsage = ctx.webServer.register({
|
|
2613
|
+
kind: "exact",
|
|
2614
|
+
path: TRAE_USAGE_PATH,
|
|
2615
|
+
handler: async (req, res) => {
|
|
2616
|
+
if (req.method !== "GET") {
|
|
2617
|
+
json(res, 405, { error: "method not allowed" });
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!loopbackOrigin(req)) {
|
|
2621
|
+
json(res, 403, { error: "origin-not-trusted" });
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
try {
|
|
2625
|
+
json(res, 200, await traeWebUsage(deps));
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
json(res, 500, { error: safeMessage(error) });
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
const disposeAccounts = ctx.webServer.register({
|
|
2632
|
+
kind: "exact",
|
|
2633
|
+
path: TRAE_ACCOUNTS_REFRESH_PATH,
|
|
2634
|
+
handler: async (req, res) => {
|
|
2635
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
2636
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
2637
|
+
try {
|
|
2638
|
+
json(res, 200, { accounts: await deps.store.accounts() });
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
json(res, 500, { error: safeMessage(error) });
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
const disposeRefresh = ctx.webServer.register({
|
|
2645
|
+
kind: "exact",
|
|
2646
|
+
path: TRAE_MODELS_REFRESH_PATH,
|
|
2647
|
+
handler: async (req, res) => {
|
|
2648
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
2649
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
2650
|
+
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
2651
|
+
try {
|
|
2652
|
+
json(res, 200, { models: await deps.discoverModels() });
|
|
2653
|
+
} catch (error) {
|
|
2654
|
+
json(res, 500, { error: safeMessage(error) });
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
});
|
|
2658
|
+
return () => {
|
|
2659
|
+
disposeRefresh();
|
|
2660
|
+
disposeAccounts();
|
|
2661
|
+
disposeUsage();
|
|
2662
|
+
};
|
|
2663
|
+
}, "dsh-connect-trae: Web usage route");
|
|
2664
|
+
}
|
|
2665
|
+
//#endregion
|
|
2666
|
+
//#region src/model-config.ts
|
|
2667
|
+
/**
|
|
2668
|
+
* Parse a secret-free CustomModel debug line emitted by Trae. Optional values
|
|
2669
|
+
* remain absent when Trae prints None; we must not invent defaults for them.
|
|
2670
|
+
*/
|
|
2671
|
+
function parseObservedModelConfig(line) {
|
|
2672
|
+
const match = /model_info: CustomModel \{([\s\S]*?)\}\s*(?:trace_id|$)/.exec(line);
|
|
2673
|
+
if (match === null) return void 0;
|
|
2674
|
+
const body = match[1] ?? "";
|
|
2675
|
+
const stringField = (name) => new RegExp(`${name}: "([^"]+)"`).exec(body)?.[1];
|
|
2676
|
+
const optionalField = (name) => new RegExp(`${name}: Some\\("([^"]+)"\\)`).exec(body)?.[1];
|
|
2677
|
+
const configName = stringField("config_name");
|
|
2678
|
+
const modelName = stringField("model_name");
|
|
2679
|
+
if (configName === void 0 || modelName === void 0) return void 0;
|
|
2680
|
+
const promptSet = optionalField("prompt_set");
|
|
2681
|
+
const rawChatFunction = optionalField("raw_chat_function");
|
|
2682
|
+
const abVersion = optionalField("ab_versions");
|
|
2683
|
+
return {
|
|
2684
|
+
configName,
|
|
2685
|
+
modelName,
|
|
2686
|
+
...promptSet === void 0 ? {} : { promptSet },
|
|
2687
|
+
...rawChatFunction === void 0 ? {} : { rawChatFunction },
|
|
2688
|
+
...abVersion === void 0 ? {} : { abVersion }
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
//#endregion
|
|
2692
|
+
//#region src/model-extra-config.ts
|
|
2693
|
+
function positive(value) {
|
|
2694
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2695
|
+
}
|
|
2696
|
+
/** Parse only Raw Chat behavior switches observed in Trae's model_extra_config. */
|
|
2697
|
+
function parseTraeRawChatBehaviorConfig(value) {
|
|
2698
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
2699
|
+
const raw = value;
|
|
2700
|
+
const maxToolcallChars = positive(raw["v3_max_toolcall_chars"]);
|
|
2701
|
+
return {
|
|
2702
|
+
...typeof raw["pass_back_reasoning"] === "boolean" ? { passBackReasoning: raw["pass_back_reasoning"] } : {},
|
|
2703
|
+
...typeof raw["native_function_call"] === "boolean" ? { nativeFunctionCall: raw["native_function_call"] } : {},
|
|
2704
|
+
...typeof raw["use_v2_process"] === "boolean" ? { useV2Process: raw["use_v2_process"] } : {},
|
|
2705
|
+
...typeof raw["v2_max_mode_enabled"] === "boolean" ? { maxModeEnabled: raw["v2_max_mode_enabled"] } : {},
|
|
2706
|
+
...maxToolcallChars === void 0 ? {} : { maxToolcallChars },
|
|
2707
|
+
...typeof raw["v3_stream_throttle_enabled"] === "boolean" ? { streamThrottleEnabled: raw["v3_stream_throttle_enabled"] } : {}
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
/** Extract the JSON object from one native model_extra_config parse log line. */
|
|
2711
|
+
function parseTraeModelExtraConfigLogLine(line) {
|
|
2712
|
+
const at = line.indexOf(", raw: ");
|
|
2713
|
+
if (at === -1) return void 0;
|
|
2714
|
+
const candidate = line.slice(at + 7).trim();
|
|
2715
|
+
try {
|
|
2716
|
+
return parseTraeRawChatBehaviorConfig(JSON.parse(candidate));
|
|
2717
|
+
} catch {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
//#endregion
|
|
2722
|
+
//#region src/model-detail.ts
|
|
2723
|
+
const TRAE_MODEL_DETAIL_PATH = "/api/ide/v1/batch_get_detail_param";
|
|
2724
|
+
const TRAE_MODEL_DETAIL_FUNCTIONS = [
|
|
2725
|
+
"ui_builder_v2",
|
|
2726
|
+
"solo_coder",
|
|
2727
|
+
"chat_v3",
|
|
2728
|
+
"solo_builder",
|
|
2729
|
+
"builder_v3",
|
|
2730
|
+
"builder",
|
|
2731
|
+
"chat",
|
|
2732
|
+
"inline_chat",
|
|
2733
|
+
"git_ai",
|
|
2734
|
+
"custom_agent_generation",
|
|
2735
|
+
"utils",
|
|
2736
|
+
"code_reviewer",
|
|
2737
|
+
"code_review_summary",
|
|
2738
|
+
"solo_agent",
|
|
2739
|
+
"solo_agent_remote",
|
|
2740
|
+
"solo_work_remote",
|
|
2741
|
+
"solo_agent_lite",
|
|
2742
|
+
"solo_work_lite",
|
|
2743
|
+
"solo_design_lite",
|
|
2744
|
+
"solo_design_remote",
|
|
2745
|
+
"multimodal",
|
|
2746
|
+
"system_diagnosis"
|
|
2747
|
+
];
|
|
2748
|
+
/** Build the exact request shape observed in successful current Trae CN logs. */
|
|
2749
|
+
function buildTraeModelDetailRequest(configName = "", custom = false) {
|
|
2750
|
+
return {
|
|
2751
|
+
functions: [...TRAE_MODEL_DETAIL_FUNCTIONS],
|
|
2752
|
+
agent_type: configName === "" ? "" : "solo_agent",
|
|
2753
|
+
current_config_info: {
|
|
2754
|
+
config_name: configName,
|
|
2755
|
+
is_custom_model: custom
|
|
2756
|
+
},
|
|
2757
|
+
mode_type: 0,
|
|
2758
|
+
access_type: 0,
|
|
2759
|
+
ab_force_vids: "",
|
|
2760
|
+
ab_autotest_advanced_mode: 0
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
//#endregion
|
|
2764
|
+
//#region src/raw-envelope.ts
|
|
2765
|
+
/** Stable JSON hash candidate used by the native DTO's arg/args_hash fields. */
|
|
2766
|
+
function hashTraeRawChatArg(arg) {
|
|
2767
|
+
return createHash("sha256").update(JSON.stringify(arg)).digest("hex");
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Evidence-bounded fusion envelope candidate. The native DTO exposes exactly
|
|
2771
|
+
* arg/args_hash/config_json, but the hash algorithm and config_json contents
|
|
2772
|
+
* remain candidates until a live response validates them.
|
|
2773
|
+
*/
|
|
2774
|
+
function buildTraeFusionRawChatEnvelope(arg, config) {
|
|
2775
|
+
return {
|
|
2776
|
+
arg: structuredClone(arg),
|
|
2777
|
+
args_hash: hashTraeRawChatArg(arg),
|
|
2778
|
+
config_json: JSON.stringify(config)
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
//#endregion
|
|
2782
|
+
//#region src/solo-remote-bridge.ts
|
|
2783
|
+
function classify(status) {
|
|
2784
|
+
if (status === 401 || status === 403) return "authentication";
|
|
2785
|
+
if (status === 402) return "hard_credit";
|
|
2786
|
+
if (status === 429) return "soft_rate";
|
|
2787
|
+
if (status === 404) return "not_found";
|
|
2788
|
+
if (status >= 500) return "server";
|
|
2789
|
+
return "client";
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Bridges the SOLO remote polling-based API into the shim's stream interface.
|
|
2793
|
+
* The remote API uses create-session + poll-messages rather than raw SSE,
|
|
2794
|
+
* so this bridge converts the final result into a synthetic OpenAI SSE stream.
|
|
2795
|
+
*/
|
|
2796
|
+
var TraeSoloRemoteBridge = class {
|
|
2797
|
+
remote;
|
|
2798
|
+
constructor(remote) {
|
|
2799
|
+
this.remote = remote;
|
|
2800
|
+
}
|
|
2801
|
+
async chatStream(bodyJson, signal) {
|
|
2802
|
+
let parsed;
|
|
2803
|
+
try {
|
|
2804
|
+
parsed = JSON.parse(bodyJson);
|
|
2805
|
+
} catch {
|
|
2806
|
+
return {
|
|
2807
|
+
ok: false,
|
|
2808
|
+
status: 400,
|
|
2809
|
+
kind: "client",
|
|
2810
|
+
message: "invalid JSON request"
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
if (!Array.isArray(parsed.messages) || parsed.messages.length === 0) return {
|
|
2814
|
+
ok: false,
|
|
2815
|
+
status: 400,
|
|
2816
|
+
kind: "client",
|
|
2817
|
+
message: "messages are required"
|
|
2818
|
+
};
|
|
2819
|
+
const messages = parsed.messages.flatMap((message) => {
|
|
2820
|
+
if (typeof message.role !== "string") return [];
|
|
2821
|
+
if (typeof message.content === "string") return [{
|
|
2822
|
+
role: message.role,
|
|
2823
|
+
content: message.content
|
|
2824
|
+
}];
|
|
2825
|
+
if (!Array.isArray(message.content)) return [];
|
|
2826
|
+
const content = message.content.flatMap((part) => {
|
|
2827
|
+
if (typeof part !== "object" || part === null) return [];
|
|
2828
|
+
const item = part;
|
|
2829
|
+
const text = typeof item.text === "string" ? item.text : typeof item.content === "string" ? item.content : void 0;
|
|
2830
|
+
return text === void 0 ? [] : [text];
|
|
2831
|
+
}).join("\n");
|
|
2832
|
+
return content === "" ? [] : [{
|
|
2833
|
+
role: message.role,
|
|
2834
|
+
content
|
|
2835
|
+
}];
|
|
2836
|
+
});
|
|
2837
|
+
if (messages.length === 0) return {
|
|
2838
|
+
ok: false,
|
|
2839
|
+
status: 400,
|
|
2840
|
+
kind: "client",
|
|
2841
|
+
message: "text messages are required"
|
|
2842
|
+
};
|
|
2843
|
+
const model = typeof parsed.model === "string" && parsed.model !== "" ? parsed.model : "DeepSeek-V4-Flash";
|
|
2844
|
+
const upstreamModel = model.endsWith("@1m") ? model.slice(0, -3) : model;
|
|
2845
|
+
try {
|
|
2846
|
+
const chunks = buildOpenAISseChunks((await this.remote.chat(messages, upstreamModel, signal)).content, model);
|
|
2847
|
+
const encoder = new TextEncoder();
|
|
2848
|
+
const stream = new ReadableStream({ start(controller) {
|
|
2849
|
+
for (const chunk of chunks) controller.enqueue(encoder.encode(chunk));
|
|
2850
|
+
controller.close();
|
|
2851
|
+
} });
|
|
2852
|
+
return {
|
|
2853
|
+
ok: true,
|
|
2854
|
+
response: new Response(stream, {
|
|
2855
|
+
status: 200,
|
|
2856
|
+
headers: { "Content-Type": "text/event-stream" }
|
|
2857
|
+
})
|
|
2858
|
+
};
|
|
2859
|
+
} catch (error) {
|
|
2860
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2861
|
+
const status = /timed out/i.test(message) ? 504 : /session creation failed/i.test(message) ? 502 : 502;
|
|
2862
|
+
return {
|
|
2863
|
+
ok: false,
|
|
2864
|
+
status,
|
|
2865
|
+
kind: classify(status),
|
|
2866
|
+
message: message.slice(0, 400)
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
function buildOpenAISseChunks(content, model) {
|
|
2872
|
+
const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
2873
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
2874
|
+
const chunks = [];
|
|
2875
|
+
if (content !== "") chunks.push(`data: ${JSON.stringify({
|
|
2876
|
+
id,
|
|
2877
|
+
object: "chat.completion.chunk",
|
|
2878
|
+
created,
|
|
2879
|
+
model,
|
|
2880
|
+
choices: [{
|
|
2881
|
+
index: 0,
|
|
2882
|
+
delta: { content },
|
|
2883
|
+
finish_reason: null
|
|
2884
|
+
}]
|
|
2885
|
+
})}\n\n`);
|
|
2886
|
+
chunks.push(`data: ${JSON.stringify({
|
|
2887
|
+
id,
|
|
2888
|
+
object: "chat.completion.chunk",
|
|
2889
|
+
created,
|
|
2890
|
+
model,
|
|
2891
|
+
choices: [{
|
|
2892
|
+
index: 0,
|
|
2893
|
+
delta: {},
|
|
2894
|
+
finish_reason: "stop"
|
|
2895
|
+
}]
|
|
2896
|
+
})}\n\n`);
|
|
2897
|
+
chunks.push("data: [DONE]\n\n");
|
|
2898
|
+
return chunks;
|
|
2899
|
+
}
|
|
2900
|
+
//#endregion
|
|
2901
|
+
//#region src/upstream.ts
|
|
2902
|
+
/**
|
|
2903
|
+
* Safe first-stage upstream. Real Trae traffic stays disabled until one
|
|
2904
|
+
* edition-specific endpoint and wire codec are verified with fixtures.
|
|
2905
|
+
*/
|
|
2906
|
+
var UnconfiguredTraeUpstreamClient = class {
|
|
2907
|
+
async chatStream(_bodyJson, _signal) {
|
|
2908
|
+
return {
|
|
2909
|
+
ok: false,
|
|
2910
|
+
status: 503,
|
|
2911
|
+
kind: "unconfigured",
|
|
2912
|
+
message: "Trae upstream protocol is not configured; complete offline protocol verification first"
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
//#endregion
|
|
2917
|
+
//#region src/index.ts
|
|
2918
|
+
const name = "llm-trae";
|
|
2919
|
+
const inject = ["llm"];
|
|
2920
|
+
const TRAE_SETTINGS_NS = settingsNamespace("trae");
|
|
2921
|
+
const modelConfig = z.object({
|
|
2922
|
+
id: z.string().required(),
|
|
2923
|
+
name: z.string().required(),
|
|
2924
|
+
contextWindow: z.number().step(1).min(1),
|
|
2925
|
+
maxTokens: z.number().step(1).min(1),
|
|
2926
|
+
input: z.array(z.union(["text", "image"]))
|
|
2927
|
+
});
|
|
2928
|
+
const Config = z.object({
|
|
2929
|
+
authFile: z.string().description("Optional Trae storage.json path override"),
|
|
2930
|
+
edition: z.union([
|
|
2931
|
+
"auto",
|
|
2932
|
+
"cn",
|
|
2933
|
+
"sg",
|
|
2934
|
+
"solo",
|
|
2935
|
+
"solo-sg"
|
|
2936
|
+
]).default("auto").description("Trae edition hint"),
|
|
2937
|
+
accountId: z.string().description("Selected local Trae account id (never a token)"),
|
|
2938
|
+
lastCatalog: z.array(modelConfig).description("Last refreshed Trae raw model directory shown by the plugin card"),
|
|
2939
|
+
enabledModelIds: z.array(z.string()).default([]).description("Trae model ids the user enabled"),
|
|
2940
|
+
enabled1mModels: z.array(z.string()).default([]).description("Trae model ids whose 1M variants are enabled"),
|
|
2941
|
+
models: z.array(modelConfig).description("Legacy generated Trae model list")
|
|
2942
|
+
});
|
|
2943
|
+
function apply(ctx, config) {
|
|
2944
|
+
const catalog = new TraeCatalog();
|
|
2945
|
+
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
2946
|
+
const enabled1mSet = (value) => new Set(value.enabled1mModels ?? []);
|
|
2947
|
+
const configuredModels = (value) => {
|
|
2948
|
+
if (value.lastCatalog?.length) return deriveCatalog(value.lastCatalog, enabledSet(value), enabled1mSet(value));
|
|
2949
|
+
return value.models?.length ? value.models : FALLBACK_TRAE_MODELS;
|
|
2950
|
+
};
|
|
2951
|
+
const displayModels = (value) => value.lastCatalog?.length ? value.lastCatalog : value.models?.length ? value.models : FALLBACK_TRAE_MODELS;
|
|
2952
|
+
const store = new TraeCredentialStore({
|
|
2953
|
+
...config.authFile === void 0 ? {} : { storagePath: config.authFile },
|
|
2954
|
+
edition: config.edition ?? "auto",
|
|
2955
|
+
...config.accountId === void 0 ? {} : { accountId: config.accountId },
|
|
2956
|
+
refresh: (credential) => refreshTraeCredential(credential)
|
|
2957
|
+
});
|
|
2958
|
+
const identity = async () => {
|
|
2959
|
+
const candidate = config.authFile === void 0 ? traeStorageCandidates().find((item) => (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : {
|
|
2960
|
+
edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
|
|
2961
|
+
path: config.authFile
|
|
2962
|
+
};
|
|
2963
|
+
if (candidate === void 0) throw new Error("Trae storage was not found");
|
|
2964
|
+
return readTraeIdentity(candidate);
|
|
2965
|
+
};
|
|
2966
|
+
const solo = new TraeSoloUpstreamClient({
|
|
2967
|
+
credential: () => store.resolve(),
|
|
2968
|
+
identity,
|
|
2969
|
+
baseUrl: "https://trae-api-cn.mchost.guru"
|
|
2970
|
+
});
|
|
2971
|
+
const remote = new TraeSoloRemoteClient({ credential: () => store.resolve() });
|
|
2972
|
+
const upstream = new TraeSoloBridge(solo);
|
|
2973
|
+
const delegating = new TraeDelegatingUpstreamClient(upstream);
|
|
2974
|
+
const shim = createTraeShim({
|
|
2975
|
+
catalog,
|
|
2976
|
+
client: delegating,
|
|
2977
|
+
logger: ctx.logger
|
|
2978
|
+
});
|
|
2979
|
+
let rawDiagnostic = () => ({ state: "disabled" });
|
|
2980
|
+
(async () => {
|
|
2981
|
+
try {
|
|
2982
|
+
const { identity, runtime } = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
|
|
2983
|
+
const gateway = createTraeRawGateway({
|
|
2984
|
+
raw: new TraeRawChatUpstreamClient({
|
|
2985
|
+
credential: () => store.resolve(),
|
|
2986
|
+
identity: async () => identity,
|
|
2987
|
+
config: {
|
|
2988
|
+
model: runtime.modelName,
|
|
2989
|
+
configName: runtime.configName,
|
|
2990
|
+
passBackReasoning: true,
|
|
2991
|
+
runtime
|
|
2992
|
+
},
|
|
2993
|
+
baseUrl: "https://trae-api-cn.mchost.guru"
|
|
2994
|
+
}),
|
|
2995
|
+
solo: upstream,
|
|
2996
|
+
endpoint: "https://trae-api-cn.mchost.guru/api/ide/v2/llm_raw_chat",
|
|
2997
|
+
edition: identity.edition,
|
|
2998
|
+
identity: {
|
|
2999
|
+
appVersion: identity.appVersion ?? "",
|
|
3000
|
+
buildVersion: identity.buildVersion ?? ""
|
|
3001
|
+
},
|
|
3002
|
+
runtime,
|
|
3003
|
+
enabled: false
|
|
3004
|
+
});
|
|
3005
|
+
delegating.replace(gateway.upstream);
|
|
3006
|
+
rawDiagnostic = () => gateway.diagnostic();
|
|
3007
|
+
ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
|
|
3010
|
+
}
|
|
3011
|
+
})();
|
|
3012
|
+
const selectPreferredGeneralAccount = async () => {
|
|
3013
|
+
try {
|
|
3014
|
+
const accounts = await store.accounts();
|
|
3015
|
+
const scored = [];
|
|
3016
|
+
for (const account of accounts) try {
|
|
3017
|
+
store.selectAccount(account.id);
|
|
3018
|
+
const credential = await store.resolve();
|
|
3019
|
+
const snapshot = await new TraeUsageClient({ credential: async () => credential }).snapshot();
|
|
3020
|
+
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;
|
|
3021
|
+
scored.push({
|
|
3022
|
+
id: account.id,
|
|
3023
|
+
general
|
|
3024
|
+
});
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
ctx.logger.warn(`dsh-connect-trae: account ${account.id} unavailable for default selection`, error);
|
|
3027
|
+
}
|
|
3028
|
+
scored.sort((a, b) => b.general - a.general);
|
|
3029
|
+
store.setPreferAccountIds(scored.filter((item) => item.general > 0).map((item) => item.id));
|
|
3030
|
+
store.setSource(config.authFile, config.edition ?? "auto", config.accountId);
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
ctx.logger.warn("dsh-connect-trae: default account selection failed; continuing", error);
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
const usageClient = new TraeUsageClient({ credential: () => store.resolve() });
|
|
3036
|
+
let current = () => config;
|
|
3037
|
+
let invalidateAdapter = () => {};
|
|
3038
|
+
const discoverModels = async (signal) => {
|
|
3039
|
+
return discoveredCatalog(await remote.fetchModels(signal), /* @__PURE__ */ new Set());
|
|
3040
|
+
};
|
|
3041
|
+
ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
|
|
3042
|
+
store,
|
|
3043
|
+
client: usageClient,
|
|
3044
|
+
displayModels: () => displayModels(current()),
|
|
3045
|
+
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
3046
|
+
enabled1mModelIds: () => current().enabled1mModels ?? [],
|
|
3047
|
+
discoverModels,
|
|
3048
|
+
rawDiagnostic: () => rawDiagnostic()
|
|
3049
|
+
}));
|
|
3050
|
+
installSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, {
|
|
3051
|
+
setSource(source) {
|
|
3052
|
+
current = source;
|
|
3053
|
+
},
|
|
3054
|
+
onChange() {
|
|
3055
|
+
const next = current();
|
|
3056
|
+
store.setSource(next.authFile, next.edition ?? "auto", next.accountId);
|
|
3057
|
+
catalog.set(configuredModels(next));
|
|
3058
|
+
invalidateAdapter();
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
let stopped = false;
|
|
3062
|
+
ctx.effect(() => () => {
|
|
3063
|
+
stopped = true;
|
|
3064
|
+
shim.close();
|
|
3065
|
+
});
|
|
3066
|
+
shim.ready.then(async () => {
|
|
3067
|
+
if (stopped) return;
|
|
3068
|
+
await selectPreferredGeneralAccount();
|
|
3069
|
+
catalog.set(configuredModels(current()));
|
|
3070
|
+
const trae = createTraeAdapter({
|
|
3071
|
+
shim,
|
|
3072
|
+
catalog,
|
|
3073
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
3074
|
+
});
|
|
3075
|
+
invalidateAdapter = () => {
|
|
3076
|
+
trae.invalidate();
|
|
3077
|
+
};
|
|
3078
|
+
let releaseAdapter;
|
|
3079
|
+
let releaseDirectory;
|
|
3080
|
+
try {
|
|
3081
|
+
releaseAdapter = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
|
|
3082
|
+
ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request) => {
|
|
3083
|
+
if (request.provider !== "trae") return [];
|
|
3084
|
+
return (await discoverModels(request.signal)).map((model) => ({
|
|
3085
|
+
id: model.id,
|
|
3086
|
+
name: model.name,
|
|
3087
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
3088
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
|
|
3089
|
+
}));
|
|
3090
|
+
});
|
|
3091
|
+
releaseDirectory = ctx.llm.registerConfigurableProviders([{
|
|
3092
|
+
provider: TRAE_PROVIDER,
|
|
3093
|
+
displayName: "Trae",
|
|
3094
|
+
settingsNs: TRAE_SETTINGS_NS,
|
|
3095
|
+
settingsPath: [],
|
|
3096
|
+
declared: false
|
|
3097
|
+
}]);
|
|
3098
|
+
} finally {
|
|
3099
|
+
if (releaseAdapter === void 0 || releaseDirectory === void 0) {
|
|
3100
|
+
releaseAdapter?.();
|
|
3101
|
+
releaseDirectory?.();
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
try {
|
|
3105
|
+
ctx.effect(() => () => {
|
|
3106
|
+
releaseAdapter?.();
|
|
3107
|
+
releaseDirectory?.();
|
|
3108
|
+
});
|
|
3109
|
+
} catch {
|
|
3110
|
+
releaseAdapter?.();
|
|
3111
|
+
releaseDirectory?.();
|
|
3112
|
+
}
|
|
3113
|
+
}).catch((error) => {
|
|
3114
|
+
ctx.logger.error("dsh-connect-trae: loopback shim failed; provider not registered", error);
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
//#endregion
|
|
3118
|
+
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_SOLO_REMOTE_MODELS, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteBridge, TraeSoloRemoteClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, discoveredCatalog, hashTraeRawChatArg, identityHeaders, inject, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeIdentity, refreshTraeCredential, registerTraeUsageRoute, resolveTraeRawRuntime, traeEndpoint, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage };
|