dsh-connect-trae 4.4.4 → 4.4.6
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 +183 -4
- package/README.en.md +17 -4
- package/README.md +28 -5
- package/lib/client.js +544 -71
- package/lib/index.d.ts +375 -3
- package/lib/index.js +623 -111
- package/package.json +14 -14
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { copyFile, readFile, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { copyFile, readFile, rename, rm, stat } from "node:fs/promises";
|
|
10
10
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
12
12
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -2223,21 +2223,55 @@ function credentialOfStored(stored) {
|
|
|
2223
2223
|
* needed and an older install's file is never rejected. A malformed row is
|
|
2224
2224
|
* dropped rather than failing the whole file — one bad imported account must
|
|
2225
2225
|
* not hide the rest.
|
|
2226
|
+
*
|
|
2227
|
+
* Returns `undefined` — NOT an empty array — for a file this build cannot parse
|
|
2228
|
+
* at all: a torn JSON write, a hand-edited document, or a version nothing here
|
|
2229
|
+
* knows. The distinction is load-bearing (see {@link readOwnForMerge}): every
|
|
2230
|
+
* merge writer treats `[]` as the merge base but `undefined` as "do not build
|
|
2231
|
+
* on this", because an unparseable file may still HOLD every account, and
|
|
2232
|
+
* merging onto "empty" would erase them on the very next refresh.
|
|
2226
2233
|
*/
|
|
2227
2234
|
function parseOwn(text) {
|
|
2235
|
+
let document;
|
|
2228
2236
|
try {
|
|
2229
|
-
const
|
|
2230
|
-
if (
|
|
2231
|
-
|
|
2232
|
-
return document.credentials.map(credentialOfStored).filter((value) => value !== void 0);
|
|
2233
|
-
}
|
|
2234
|
-
if (document.version === OWN_VERSION_SINGLE) {
|
|
2235
|
-
const single = credentialOfStored(document.credential);
|
|
2236
|
-
return single === void 0 ? [] : [single];
|
|
2237
|
-
}
|
|
2238
|
-
return [];
|
|
2237
|
+
const parsed = JSON.parse(text);
|
|
2238
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2239
|
+
document = parsed;
|
|
2239
2240
|
} catch {
|
|
2240
|
-
return
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
if (document.version === OWN_VERSION) {
|
|
2244
|
+
if (!Array.isArray(document.credentials)) return void 0;
|
|
2245
|
+
return document.credentials.map(credentialOfStored).filter((value) => value !== void 0);
|
|
2246
|
+
}
|
|
2247
|
+
if (document.version === OWN_VERSION_SINGLE) {
|
|
2248
|
+
const single = credentialOfStored(document.credential);
|
|
2249
|
+
return single === void 0 ? void 0 : [single];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Move a plugin-owned file this build can no longer parse out of the way, so
|
|
2254
|
+
* the next write starts from a clean file instead of building on — or being
|
|
2255
|
+
* refused by — the unreadable one.
|
|
2256
|
+
*
|
|
2257
|
+
* The rename is the data preservation: the bytes stay on disk beside the live
|
|
2258
|
+
* path (`.corrupt-<stamp>`), so a future build that understands the format (or
|
|
2259
|
+
* a user with a text editor) can still recover the accounts it carried. It is
|
|
2260
|
+
* best-effort on purpose: a quarantine that cannot happen must not fail the
|
|
2261
|
+
* user action that triggered it, and the merge writers refuse to overwrite a
|
|
2262
|
+
* file they could not quarantine (see {@link readOwnForMerge}).
|
|
2263
|
+
*
|
|
2264
|
+
* Must be called under the same lock the caller writes under: without the lock
|
|
2265
|
+
* the rename could interleave with a concurrent writer holding the pre-rename
|
|
2266
|
+
* content and write it right back.
|
|
2267
|
+
*/
|
|
2268
|
+
async function quarantineCorruptOwn(path) {
|
|
2269
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2270
|
+
try {
|
|
2271
|
+
await rename(path, `${path}.corrupt-${stamp}`);
|
|
2272
|
+
return true;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return false;
|
|
2241
2275
|
}
|
|
2242
2276
|
}
|
|
2243
2277
|
/**
|
|
@@ -2415,9 +2449,26 @@ var TraeCredentialStore = class {
|
|
|
2415
2449
|
return /* @__PURE__ */ new Set();
|
|
2416
2450
|
}
|
|
2417
2451
|
}
|
|
2418
|
-
|
|
2452
|
+
/**
|
|
2453
|
+
* Read, mutate, and write the tombstone list under ONE lock.
|
|
2454
|
+
*
|
|
2455
|
+
* The tombstone file is read-modify-write state shared by two flows that run
|
|
2456
|
+
* concurrently: `mirrorLocalLogins` (drops tombstones whose logins are gone)
|
|
2457
|
+
* and `deleteAccount` (adds the id just deleted). A read OUTSIDE the write's
|
|
2458
|
+
* lock loses updates in both directions — a dropped-add resurrects a deleted
|
|
2459
|
+
* account on the next mirror, a dropped-drop keeps a tombstone forever and
|
|
2460
|
+
* hides every future sign-in of that account. The mutator therefore runs on
|
|
2461
|
+
* the freshly-read set INSIDE the lock, and what it returns is what gets
|
|
2462
|
+
* written, with no window in between.
|
|
2463
|
+
*
|
|
2464
|
+
* Fire-and-forget safe: a failed update only delays a tombstone change until
|
|
2465
|
+
* the next mirror or delete, so errors are swallowed like the readers'.
|
|
2466
|
+
*/
|
|
2467
|
+
async updateTombstones(mutator) {
|
|
2419
2468
|
const path = this.deletedPath();
|
|
2420
2469
|
await withFileLock(path, async () => {
|
|
2470
|
+
const ids = await this.readTombstones();
|
|
2471
|
+
mutator(ids);
|
|
2421
2472
|
await writeFileAtomic(path, `${JSON.stringify({
|
|
2422
2473
|
version: 1,
|
|
2423
2474
|
ids: [...ids]
|
|
@@ -2425,7 +2476,31 @@ var TraeCredentialStore = class {
|
|
|
2425
2476
|
mode: 384,
|
|
2426
2477
|
dirMode: 448
|
|
2427
2478
|
});
|
|
2428
|
-
});
|
|
2479
|
+
}).catch(() => void 0);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Read the plugin-owned file as the merge base for a read-modify-write.
|
|
2483
|
+
*
|
|
2484
|
+
* This is the fail-safe that keeps one unreadable file from erasing every
|
|
2485
|
+
* account: `parseOwn` now distinguishes a VALID empty file (`[]`) from one
|
|
2486
|
+
* this build cannot parse (`undefined`), and only the former may serve as a
|
|
2487
|
+
* merge base. For the latter the file is quarantined — renamed aside with
|
|
2488
|
+
* its bytes intact — and the merge then starts from nothing, so the action
|
|
2489
|
+
* the user pressed still succeeds and the lost entries are recoverable from
|
|
2490
|
+
* the quarantine copy instead of being overwritten silently. A quarantine
|
|
2491
|
+
* that fails (an exotic filesystem, a name collision) aborts the write
|
|
2492
|
+
* instead: losing one refresh is recoverable by pressing it again, losing
|
|
2493
|
+
* every account is not.
|
|
2494
|
+
*
|
|
2495
|
+
* Every caller must hold the {@link withFileLock} of `path` across BOTH the
|
|
2496
|
+
* read and the subsequent write — this does not lock by itself.
|
|
2497
|
+
*/
|
|
2498
|
+
async readOwnForMerge(path) {
|
|
2499
|
+
const text = await readFile(path, "utf8").catch(() => void 0);
|
|
2500
|
+
if (text === void 0) return [];
|
|
2501
|
+
const parsed = parseOwn(text);
|
|
2502
|
+
if (parsed !== void 0) return parsed;
|
|
2503
|
+
return await quarantineCorruptOwn(path) ? [] : void 0;
|
|
2429
2504
|
}
|
|
2430
2505
|
/**
|
|
2431
2506
|
* Copy every local Trae sign-in into this store's own file, once.
|
|
@@ -2447,20 +2522,17 @@ var TraeCredentialStore = class {
|
|
|
2447
2522
|
const { credentials: discovered } = await this.readDesktopAll();
|
|
2448
2523
|
const own = await this.readOwns();
|
|
2449
2524
|
const ownIds = new Set(own.map((credential) => traeAccountId(credential)));
|
|
2450
|
-
const tombstones = await this.readTombstones();
|
|
2451
2525
|
const liveIds = new Set(discovered.map((credential) => traeAccountId(credential)));
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
if (liveIds.has(id))
|
|
2455
|
-
|
|
2456
|
-
tombstonesChanged = true;
|
|
2457
|
-
}
|
|
2526
|
+
const tombstones = await this.readTombstones();
|
|
2527
|
+
if ([...tombstones].some((id) => !liveIds.has(id))) await this.updateTombstones((ids) => {
|
|
2528
|
+
for (const id of [...ids]) if (!liveIds.has(id)) ids.delete(id);
|
|
2529
|
+
});
|
|
2458
2530
|
const missing = discovered.filter((credential) => this.matchesRegion(credential) && !ownIds.has(traeAccountId(credential)) && !tombstones.has(traeAccountId(credential)));
|
|
2459
|
-
if (tombstonesChanged) await this.writeTombstones(tombstones);
|
|
2460
2531
|
if (missing.length === 0) return;
|
|
2461
2532
|
const ownPath = this.ownAuthPath();
|
|
2462
2533
|
await withFileLock(ownPath, async () => {
|
|
2463
|
-
const existing = await
|
|
2534
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2535
|
+
if (existing === void 0) return;
|
|
2464
2536
|
const byId = new Map(existing.map((credential) => [traeAccountId(credential), credential]));
|
|
2465
2537
|
for (const credential of missing) {
|
|
2466
2538
|
const id = traeAccountId(credential);
|
|
@@ -2748,7 +2820,8 @@ var TraeCredentialStore = class {
|
|
|
2748
2820
|
const ownPath = this.ownAuthPath();
|
|
2749
2821
|
const written = [];
|
|
2750
2822
|
await withFileLock(ownPath, async () => {
|
|
2751
|
-
const existing = await
|
|
2823
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2824
|
+
if (existing === void 0) return;
|
|
2752
2825
|
const byId = /* @__PURE__ */ new Map();
|
|
2753
2826
|
for (const item of existing) byId.set(traeAccountId(item), item);
|
|
2754
2827
|
for (const credential of credentials) {
|
|
@@ -2907,7 +2980,7 @@ var TraeCredentialStore = class {
|
|
|
2907
2980
|
let mutated = false;
|
|
2908
2981
|
for (const path of this.ownCandidates()) {
|
|
2909
2982
|
const dropped = await withFileLock(path, async () => {
|
|
2910
|
-
const existing = await readFile(path, "utf8").then(parseOwn, () => null);
|
|
2983
|
+
const existing = await readFile(path, "utf8").then((text) => parseOwn(text) ?? null, () => null);
|
|
2911
2984
|
if (existing === null || !existing.some((credential) => traeAccountId(credential) === accountId)) return false;
|
|
2912
2985
|
const next = existing.filter((credential) => traeAccountId(credential) !== accountId);
|
|
2913
2986
|
await writeOwn(path, next);
|
|
@@ -2915,11 +2988,9 @@ var TraeCredentialStore = class {
|
|
|
2915
2988
|
}).catch(() => false);
|
|
2916
2989
|
mutated = mutated || dropped;
|
|
2917
2990
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
await this.writeTombstones(tombstones).catch(() => void 0);
|
|
2922
|
-
}
|
|
2991
|
+
await this.updateTombstones((ids) => {
|
|
2992
|
+
ids.add(accountId);
|
|
2993
|
+
});
|
|
2923
2994
|
if (this.accountId === accountId) {
|
|
2924
2995
|
this.accountId = void 0;
|
|
2925
2996
|
this.inflight = void 0;
|
|
@@ -3039,7 +3110,8 @@ var TraeCredentialStore = class {
|
|
|
3039
3110
|
async readOwns() {
|
|
3040
3111
|
const copies = [];
|
|
3041
3112
|
for (const path of this.ownCandidates()) try {
|
|
3042
|
-
|
|
3113
|
+
const parsed = parseOwn(await readFile(path, "utf8"));
|
|
3114
|
+
if (parsed !== void 0) copies.push(...parsed);
|
|
3043
3115
|
} catch {}
|
|
3044
3116
|
return copies;
|
|
3045
3117
|
}
|
|
@@ -3061,7 +3133,8 @@ var TraeCredentialStore = class {
|
|
|
3061
3133
|
};
|
|
3062
3134
|
const ownPath = this.ownAuthPath();
|
|
3063
3135
|
await withFileLock(ownPath, async () => {
|
|
3064
|
-
const existing = await
|
|
3136
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
3137
|
+
if (existing === void 0) return;
|
|
3065
3138
|
const refreshedId = traeAccountId(refreshed);
|
|
3066
3139
|
const entries = existing.filter((item) => traeAccountId(item) !== refreshedId);
|
|
3067
3140
|
entries.push(refreshed);
|
|
@@ -3513,6 +3586,182 @@ function identityHeaders(identity) {
|
|
|
3513
3586
|
};
|
|
3514
3587
|
}
|
|
3515
3588
|
//#endregion
|
|
3589
|
+
//#region src/model-order.ts
|
|
3590
|
+
/**
|
|
3591
|
+
* Wire name of the default SOLO chat function.
|
|
3592
|
+
*
|
|
3593
|
+
* Lives here rather than in `solo.ts` so this module stays importable from the
|
|
3594
|
+
* client; `solo.ts` re-exports it, so there is still exactly one definition.
|
|
3595
|
+
*/
|
|
3596
|
+
const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
3597
|
+
/**
|
|
3598
|
+
* The chat functions of one region, in PRIORITY order.
|
|
3599
|
+
*
|
|
3600
|
+
* Two consumers read it, and both mean "priority", which is why they can share
|
|
3601
|
+
* one array: the wire asks these functions in order and the first to list a
|
|
3602
|
+
* config owns it (`solo.ts`), and the card draws the groups in the same order so
|
|
3603
|
+
* the row a user sees first is the one the resolver prefers.
|
|
3604
|
+
*/
|
|
3605
|
+
const TRAE_CHANNEL_PRIORITY = {
|
|
3606
|
+
cn: [
|
|
3607
|
+
"solo_work_remote",
|
|
3608
|
+
TRAE_SOLO_FUNCTION,
|
|
3609
|
+
"solo_agent_remote",
|
|
3610
|
+
"chat_v3",
|
|
3611
|
+
"solo_coder"
|
|
3612
|
+
],
|
|
3613
|
+
ai: [
|
|
3614
|
+
"solo_work_remote",
|
|
3615
|
+
TRAE_SOLO_FUNCTION,
|
|
3616
|
+
"solo_agent_remote",
|
|
3617
|
+
"chat_v3"
|
|
3618
|
+
]
|
|
3619
|
+
};
|
|
3620
|
+
/**
|
|
3621
|
+
* Rank of one channel inside the region's priority list.
|
|
3622
|
+
*
|
|
3623
|
+
* Three cases, and the order between them is deliberate:
|
|
3624
|
+
*
|
|
3625
|
+
* - a known chat channel: its index in the priority list;
|
|
3626
|
+
* - an UNKNOWN channel name (a function the region does not call through —
|
|
3627
|
+
* `withChatCallableFunctions` clears those, so this is a defensive branch):
|
|
3628
|
+
* after every known channel, then alphabetically among themselves;
|
|
3629
|
+
* - NO channel at all (a row demoted to the default chat function): last. Such
|
|
3630
|
+
* a row is the one the resolver knows least about, so it must not push a
|
|
3631
|
+
* properly attributed row down the list.
|
|
3632
|
+
*/
|
|
3633
|
+
function channelRank(channel, priority) {
|
|
3634
|
+
if (channel === void 0 || channel === "") return priority.length + 1;
|
|
3635
|
+
const known = priority.indexOf(channel);
|
|
3636
|
+
return known === -1 ? priority.length : known;
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Compare two display names the way a person reading a model list expects:
|
|
3640
|
+
* case-insensitively, with embedded numbers compared as NUMBERS, so `GLM-5.10`
|
|
3641
|
+
* sorts after `GLM-5.9`. Plain text comparison puts `5.10` first, which reads as
|
|
3642
|
+
* a bug to anyone who sees the version numbers.
|
|
3643
|
+
*/
|
|
3644
|
+
function compareNames(left, right) {
|
|
3645
|
+
const byName = left.trim().localeCompare(right.trim(), void 0, {
|
|
3646
|
+
numeric: true,
|
|
3647
|
+
sensitivity: "base"
|
|
3648
|
+
});
|
|
3649
|
+
return byName !== 0 ? byName : left.localeCompare(right);
|
|
3650
|
+
}
|
|
3651
|
+
/**
|
|
3652
|
+
* The catalog in display order: channel priority first, then name.
|
|
3653
|
+
*
|
|
3654
|
+
* Total and stable — ties fall through to `id`, so two rows with the same name
|
|
3655
|
+
* in the same channel still get one fixed order rather than the order they
|
|
3656
|
+
* arrived in.
|
|
3657
|
+
*/
|
|
3658
|
+
function orderTraeModels(rows, region = "cn") {
|
|
3659
|
+
const priority = TRAE_CHANNEL_PRIORITY[region] ?? TRAE_CHANNEL_PRIORITY.cn;
|
|
3660
|
+
return [...rows].sort((left, right) => {
|
|
3661
|
+
const byRank = channelRank(left.wireFunction, priority) - channelRank(right.wireFunction, priority);
|
|
3662
|
+
if (byRank !== 0) return byRank;
|
|
3663
|
+
if (left.wireFunction !== right.wireFunction) return (left.wireFunction ?? "").localeCompare(right.wireFunction ?? "");
|
|
3664
|
+
const byName = compareNames(left.name, right.name);
|
|
3665
|
+
return byName !== 0 ? byName : left.id.localeCompare(right.id);
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Split rows into channel groups, in display order.
|
|
3670
|
+
*
|
|
3671
|
+
* Ordering is re-applied here rather than trusted from the caller: a group list
|
|
3672
|
+
* built from an unsorted input would put the groups in arrival order, which is
|
|
3673
|
+
* the exact instability this module exists to remove.
|
|
3674
|
+
*/
|
|
3675
|
+
function groupTraeModels(rows, region = "cn") {
|
|
3676
|
+
const groups = [];
|
|
3677
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
3678
|
+
for (const row of orderTraeModels(rows, region)) {
|
|
3679
|
+
const channel = row.wireFunction === "" ? void 0 : row.wireFunction;
|
|
3680
|
+
const key = channel ?? "\0default";
|
|
3681
|
+
const existing = byChannel.get(key);
|
|
3682
|
+
if (existing !== void 0) {
|
|
3683
|
+
existing.rows.push(row);
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
const group = {
|
|
3687
|
+
channel,
|
|
3688
|
+
rows: [row]
|
|
3689
|
+
};
|
|
3690
|
+
byChannel.set(key, group);
|
|
3691
|
+
groups.push(group);
|
|
3692
|
+
}
|
|
3693
|
+
return groups;
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Does one row match a search query?
|
|
3697
|
+
*
|
|
3698
|
+
* Matches the display name, the display id and the WIRE config name — the last
|
|
3699
|
+
* one because a user debugging a `4001 config_name rejected` arrives with the
|
|
3700
|
+
* upstream name in hand, and the two differ exactly on the rows that carry a
|
|
3701
|
+
* suffix (`DeepSeek-V4-Flash` vs `DeepSeek-V4-Flash-Official`).
|
|
3702
|
+
*/
|
|
3703
|
+
function traeModelMatchesQuery(model, query) {
|
|
3704
|
+
const needle = query.trim().toLowerCase();
|
|
3705
|
+
if (needle === "") return true;
|
|
3706
|
+
if (model.name.toLowerCase().includes(needle)) return true;
|
|
3707
|
+
if (model.id.toLowerCase().includes(needle)) return true;
|
|
3708
|
+
return (model.wireConfigName ?? "").toLowerCase().includes(needle);
|
|
3709
|
+
}
|
|
3710
|
+
/** Filter by {@link traeModelMatchesQuery}, preserving order. */
|
|
3711
|
+
function filterTraeModels(rows, query) {
|
|
3712
|
+
if (query.trim() === "") return [...rows];
|
|
3713
|
+
return rows.filter((row) => traeModelMatchesQuery(row, query));
|
|
3714
|
+
}
|
|
3715
|
+
/**
|
|
3716
|
+
* The channels a NORMAL Trae chat session actually runs on: the `solo_work_*`
|
|
3717
|
+
* family, i.e. the panel's own chat modes.
|
|
3718
|
+
*
|
|
3719
|
+
* Deliberately not "every channel we can call". After issue #52 P1 the catalog
|
|
3720
|
+
* is 34 rows and 10 of them are reachable only through a SPECIALIST channel:
|
|
3721
|
+
* `solo_coder` (7: `Doubao-Seed-2.0-Code`, `glm-5.1`, `glm-5v-turbo`,
|
|
3722
|
+
* `kimi-k2.5`, `minimax-m2.7`, `qwen-3.5`, `qwen-3.6-plus`), `chat_v3` (2:
|
|
3723
|
+
* `kimi-k2.8-preview`, `qwen3.8-flash`) and `solo_agent_remote` (1:
|
|
3724
|
+
* `Doubao-Seed-Code`). Measured 2026-10-23 on a live CN account. Those rows work
|
|
3725
|
+
* — that is why P1 kept them — but they are not what Trae's main chat offers,
|
|
3726
|
+
* and mixing them into the default view is exactly the "a code model answering a
|
|
3727
|
+
* solo request" shape issue #52 asked about.
|
|
3728
|
+
*/
|
|
3729
|
+
const TRAE_COMMON_FUNCTIONS = {
|
|
3730
|
+
cn: ["solo_work_remote", TRAE_SOLO_FUNCTION],
|
|
3731
|
+
ai: ["solo_work_remote", TRAE_SOLO_FUNCTION]
|
|
3732
|
+
};
|
|
3733
|
+
/**
|
|
3734
|
+
* The function a request for this row will NAME.
|
|
3735
|
+
*
|
|
3736
|
+
* A row with no `wireFunction` is not "unknown to the wire": the bridge sends
|
|
3737
|
+
* the region default (`TRAE_SOLO_FUNCTION`), so for every question about where
|
|
3738
|
+
* the turn goes, the default is the answer. The distinction still matters for
|
|
3739
|
+
* DISPLAY (`groupTraeModels` keeps these in their own group, because their
|
|
3740
|
+
* provenance — which directory group advertised them — really is unknown), but
|
|
3741
|
+
* not for scope: a `solo_work_lite` turn is a normal chat turn either way.
|
|
3742
|
+
*/
|
|
3743
|
+
function traeModelCallChannel(model) {
|
|
3744
|
+
const channel = model.wireFunction;
|
|
3745
|
+
return channel === void 0 || channel === "" ? TRAE_SOLO_FUNCTION : channel;
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Does this row belong to the view?
|
|
3749
|
+
*
|
|
3750
|
+
* Judged on {@link traeModelCallChannel}, never on `wireFunction` directly:
|
|
3751
|
+
* otherwise the one row without a function (`doubao_1_8`, measured 2026-10-23)
|
|
3752
|
+
* would fall OUT of the common view while being called through exactly the
|
|
3753
|
+
* common channel it belongs to.
|
|
3754
|
+
*/
|
|
3755
|
+
function traeModelInView(model, view, region = "cn") {
|
|
3756
|
+
if (view === "all") return true;
|
|
3757
|
+
return (TRAE_COMMON_FUNCTIONS[region] ?? TRAE_COMMON_FUNCTIONS.cn).includes(traeModelCallChannel(model));
|
|
3758
|
+
}
|
|
3759
|
+
/** Keep the rows a view asks for, preserving order. */
|
|
3760
|
+
function scopeTraeModels(rows, view, region = "cn") {
|
|
3761
|
+
if (view === "all") return [...rows];
|
|
3762
|
+
return rows.filter((row) => traeModelInView(row, view, region));
|
|
3763
|
+
}
|
|
3764
|
+
//#endregion
|
|
3516
3765
|
//#region src/shim.ts
|
|
3517
3766
|
const BODY_LIMIT = 67108864;
|
|
3518
3767
|
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -3772,6 +4021,48 @@ function createTraeShim(options) {
|
|
|
3772
4021
|
};
|
|
3773
4022
|
}
|
|
3774
4023
|
//#endregion
|
|
4024
|
+
//#region src/model-detail.ts
|
|
4025
|
+
const TRAE_MODEL_DETAIL_PATH = "/api/ide/v1/batch_get_detail_param";
|
|
4026
|
+
const TRAE_MODEL_DETAIL_FUNCTIONS = [
|
|
4027
|
+
"ui_builder_v2",
|
|
4028
|
+
"solo_coder",
|
|
4029
|
+
"chat_v3",
|
|
4030
|
+
"solo_builder",
|
|
4031
|
+
"builder_v3",
|
|
4032
|
+
"builder",
|
|
4033
|
+
"chat",
|
|
4034
|
+
"inline_chat",
|
|
4035
|
+
"git_ai",
|
|
4036
|
+
"custom_agent_generation",
|
|
4037
|
+
"utils",
|
|
4038
|
+
"code_reviewer",
|
|
4039
|
+
"code_review_summary",
|
|
4040
|
+
"solo_agent",
|
|
4041
|
+
"solo_agent_remote",
|
|
4042
|
+
"solo_work_remote",
|
|
4043
|
+
"solo_agent_lite",
|
|
4044
|
+
"solo_work_lite",
|
|
4045
|
+
"solo_design_lite",
|
|
4046
|
+
"solo_design_remote",
|
|
4047
|
+
"multimodal",
|
|
4048
|
+
"system_diagnosis"
|
|
4049
|
+
];
|
|
4050
|
+
/** Build the exact request shape observed in successful current Trae CN logs. */
|
|
4051
|
+
function buildTraeModelDetailRequest(configName = "", custom = false) {
|
|
4052
|
+
return {
|
|
4053
|
+
functions: [...TRAE_MODEL_DETAIL_FUNCTIONS],
|
|
4054
|
+
agent_type: configName === "" ? "" : "solo_agent",
|
|
4055
|
+
current_config_info: {
|
|
4056
|
+
config_name: configName,
|
|
4057
|
+
is_custom_model: custom
|
|
4058
|
+
},
|
|
4059
|
+
mode_type: 0,
|
|
4060
|
+
access_type: 0,
|
|
4061
|
+
ab_force_vids: "",
|
|
4062
|
+
ab_autotest_advanced_mode: 0
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
//#endregion
|
|
3775
4066
|
//#region src/protocol.ts
|
|
3776
4067
|
const TRAE_CN_AGENT_TASK_PATH = "/api/agent/v3/create_agent_task";
|
|
3777
4068
|
const TRAE_CN_TITLE_PATH = "/api/agent/v3/llm_utils_chat";
|
|
@@ -3875,6 +4166,231 @@ function traeEndpoint(baseUrl, path) {
|
|
|
3875
4166
|
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
3876
4167
|
}
|
|
3877
4168
|
//#endregion
|
|
4169
|
+
//#region src/official-roster.ts
|
|
4170
|
+
const CHAT_USAGE = "chat_completion";
|
|
4171
|
+
/**
|
|
4172
|
+
* Parse a `batch_get_detail_param` answer into a per-(config, function) index.
|
|
4173
|
+
*
|
|
4174
|
+
* Tolerant on purpose: this document is 1.8 MB of upstream-owned shape
|
|
4175
|
+
* (`function_configs[] → config_info_list[] → model_detail_list[]`), and the
|
|
4176
|
+
* plugin must not lose the whole refresh because one nested field changed
|
|
4177
|
+
* spelling. Rows that cannot be read are skipped; a document with no
|
|
4178
|
+
* `function_configs` at all yields an empty roster, which the filter reads as
|
|
4179
|
+
* "nothing to say" rather than "hide everything".
|
|
4180
|
+
*
|
|
4181
|
+
* Duplicate pairs are collapsed first-wins, matching the rest of the catalog's
|
|
4182
|
+
* convention (`mergeTraeModelSources`). Two functions can both list a config,
|
|
4183
|
+
* and those are DIFFERENT pairs, so both are kept.
|
|
4184
|
+
*/
|
|
4185
|
+
function parseTraeOfficialRoster(payload) {
|
|
4186
|
+
const channels = /* @__PURE__ */ new Map();
|
|
4187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4188
|
+
let pairs = 0;
|
|
4189
|
+
const document = typeof payload === "object" && payload !== null ? payload : {};
|
|
4190
|
+
const groups = Array.isArray(document["function_configs"]) ? document["function_configs"] : [];
|
|
4191
|
+
for (const group of groups) {
|
|
4192
|
+
if (typeof group !== "object" || group === null) continue;
|
|
4193
|
+
const record = group;
|
|
4194
|
+
const functionName = typeof record["function"] === "string" ? record["function"] : "";
|
|
4195
|
+
if (functionName === "") continue;
|
|
4196
|
+
const list = Array.isArray(record["config_info_list"]) ? record["config_info_list"] : [];
|
|
4197
|
+
for (const item of list) {
|
|
4198
|
+
if (typeof item !== "object" || item === null) continue;
|
|
4199
|
+
const config = item;
|
|
4200
|
+
const configName = typeof config["config_name"] === "string" ? config["config_name"].trim() : "";
|
|
4201
|
+
if (configName === "") continue;
|
|
4202
|
+
const key = `${functionName}\u0000${configName.toLowerCase()}`;
|
|
4203
|
+
if (seen.has(key)) continue;
|
|
4204
|
+
seen.add(key);
|
|
4205
|
+
const channel = {
|
|
4206
|
+
function: functionName,
|
|
4207
|
+
usage: typeof config["usage"] === "string" ? config["usage"] : "",
|
|
4208
|
+
enabled: config["config_switch"] !== false,
|
|
4209
|
+
invisible: config["is_invisible_to_user"] === true
|
|
4210
|
+
};
|
|
4211
|
+
pairs += 1;
|
|
4212
|
+
const bucket = channels.get(configName.toLowerCase());
|
|
4213
|
+
if (bucket === void 0) channels.set(configName.toLowerCase(), [channel]);
|
|
4214
|
+
else bucket.push(channel);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
return {
|
|
4218
|
+
channels,
|
|
4219
|
+
size: {
|
|
4220
|
+
configs: channels.size,
|
|
4221
|
+
pairs
|
|
4222
|
+
}
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
/** The `config_name` a catalog row would actually send (see `wireConfigName`). */
|
|
4226
|
+
function officialKeyOf(model) {
|
|
4227
|
+
return (model.wireConfigName ?? model.id).trim().toLowerCase();
|
|
4228
|
+
}
|
|
4229
|
+
/**
|
|
4230
|
+
* Decide one row from the roster.
|
|
4231
|
+
*
|
|
4232
|
+
* `chatFunctions` is the region's own `TRAE_DIRECTORY_FUNCTIONS` list, in
|
|
4233
|
+
* priority order: only those functions can carry a VERDICT, because only those
|
|
4234
|
+
* are functions `llm_utils_chat` accepts. A config the vendor shows under
|
|
4235
|
+
* `code_review_summary` alone is not offered as a chat model at all — that is
|
|
4236
|
+
* exactly the `glm-4.7` case, and it is a "hide", not a "re-pin".
|
|
4237
|
+
*
|
|
4238
|
+
* The verdict is `keep` when the roster cannot speak: no entry for the config
|
|
4239
|
+
* (measured 2026-10-23: `kimi-k2.8-preview` and `qwen3.8-flash` exist only in
|
|
4240
|
+
* the Remote directory's `chat_v3` group), only tool rows (`usage: 'summary'`,
|
|
4241
|
+
* `fast_apply`, …), or no chat function at all. Silence is never a verdict.
|
|
4242
|
+
*/
|
|
4243
|
+
function officialVerdictOf(model, roster, chatFunctions) {
|
|
4244
|
+
const entries = roster.channels.get(officialKeyOf(model));
|
|
4245
|
+
if (entries === void 0 || entries.length === 0) return { kind: "keep" };
|
|
4246
|
+
const callable = entries.filter((entry) => entry.usage === "" || entry.usage === CHAT_USAGE).filter((entry) => chatFunctions.includes(entry.function));
|
|
4247
|
+
if (callable.length === 0) return { kind: "keep" };
|
|
4248
|
+
const listedBy = callable.map((entry) => entry.function);
|
|
4249
|
+
const shown = callable.filter((entry) => !entry.invisible);
|
|
4250
|
+
if (shown.length === 0) return {
|
|
4251
|
+
kind: "hide",
|
|
4252
|
+
function: model.wireFunction,
|
|
4253
|
+
listedBy
|
|
4254
|
+
};
|
|
4255
|
+
const own = model.wireFunction === void 0 ? void 0 : callable.find((entry) => entry.function === model.wireFunction);
|
|
4256
|
+
if (own !== void 0 && !own.invisible) return { kind: "keep" };
|
|
4257
|
+
const to = chatFunctions.find((fn) => shown.some((entry) => entry.function === fn));
|
|
4258
|
+
if (to === void 0) return { kind: "keep" };
|
|
4259
|
+
return {
|
|
4260
|
+
kind: "repin",
|
|
4261
|
+
from: model.wireFunction,
|
|
4262
|
+
to,
|
|
4263
|
+
listedBy
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Apply the roster: retire what no chat function offers any more, and re-pin
|
|
4268
|
+
* what the vendor offers under a different channel.
|
|
4269
|
+
*
|
|
4270
|
+
* FAIL-OPEN BY CONSTRUCTION, which is the whole safety argument for applying
|
|
4271
|
+
* this by default:
|
|
4272
|
+
*
|
|
4273
|
+
* - `undefined` roster (transport failure, restricted network, a credential
|
|
4274
|
+
* that cannot read the IDE document) keeps EVERY row. A refresh must never
|
|
4275
|
+
* lose models because a second, optional endpoint was unreachable.
|
|
4276
|
+
* - `keep` covers every ambiguity — no entry, tool-only entries, a channel the
|
|
4277
|
+
* roster does not name. Only an explicit "the vendor shows this nowhere we
|
|
4278
|
+
* can call it" removes a row.
|
|
4279
|
+
* - `config_switch === false` alone does NOT remove a row: the flag is
|
|
4280
|
+
* recorded on {@link TraeOfficialChannel} but not used as a verdict, because
|
|
4281
|
+
* its semantics are unmeasured and "disabled" there may describe a function
|
|
4282
|
+
* override rather than the model itself.
|
|
4283
|
+
*
|
|
4284
|
+
* This is deliberately NOT "match the Trae client": the client renders one
|
|
4285
|
+
* function, the plugin serves the union, and issue #46's reporter wants the
|
|
4286
|
+
* dated snapshots the union carries. What changes here is only the class where
|
|
4287
|
+
* the union is provably wrong — twelve retired configs that made up the reported
|
|
4288
|
+
* "tail of old models", plus rows pinned to a channel their own vendor hides
|
|
4289
|
+
* them on (the "code model on a solo channel" risk of issue #52).
|
|
4290
|
+
*/
|
|
4291
|
+
function applyOfficialRoster(rows, roster, chatFunctions) {
|
|
4292
|
+
if (roster === void 0) return {
|
|
4293
|
+
rows: [...rows],
|
|
4294
|
+
hidden: [],
|
|
4295
|
+
repinned: []
|
|
4296
|
+
};
|
|
4297
|
+
const kept = [];
|
|
4298
|
+
const hidden = [];
|
|
4299
|
+
const repinned = [];
|
|
4300
|
+
for (const row of rows) {
|
|
4301
|
+
const verdict = officialVerdictOf(row, roster, chatFunctions);
|
|
4302
|
+
if (verdict.kind === "hide") {
|
|
4303
|
+
hidden.push({
|
|
4304
|
+
id: row.id,
|
|
4305
|
+
function: verdict.function
|
|
4306
|
+
});
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4309
|
+
if (verdict.kind === "repin") {
|
|
4310
|
+
repinned.push({
|
|
4311
|
+
id: row.id,
|
|
4312
|
+
from: verdict.from,
|
|
4313
|
+
to: verdict.to
|
|
4314
|
+
});
|
|
4315
|
+
kept.push({
|
|
4316
|
+
...row,
|
|
4317
|
+
wireFunction: verdict.to
|
|
4318
|
+
});
|
|
4319
|
+
continue;
|
|
4320
|
+
}
|
|
4321
|
+
kept.push(row);
|
|
4322
|
+
}
|
|
4323
|
+
return {
|
|
4324
|
+
rows: kept,
|
|
4325
|
+
hidden,
|
|
4326
|
+
repinned
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
/** How long one successful roster read stays usable, in milliseconds. */
|
|
4330
|
+
const ROSTER_CACHE_MS = 6e5;
|
|
4331
|
+
/** Timeout for the roster read. It is an OPTIONAL signal; it must not stall a refresh. */
|
|
4332
|
+
const ROSTER_TIMEOUT_MS = 2e4;
|
|
4333
|
+
/**
|
|
4334
|
+
* Reads the IDE's roster once per cache window and never throws.
|
|
4335
|
+
*
|
|
4336
|
+
* `undefined` is a first-class answer: it is what a caller sees when the
|
|
4337
|
+
* document could not be read, and {@link applyOfficialRoster} treats it as "no
|
|
4338
|
+
* opinion". Silence on failure is intentional — this is a refinement of a
|
|
4339
|
+
* refresh that already succeeded, so a strict error would turn a usable catalog
|
|
4340
|
+
* into a failed refresh.
|
|
4341
|
+
*/
|
|
4342
|
+
var TraeOfficialRosterClient = class {
|
|
4343
|
+
options;
|
|
4344
|
+
fetchImpl;
|
|
4345
|
+
now;
|
|
4346
|
+
cached;
|
|
4347
|
+
/** The last failure, so a repeatedly-broken endpoint is not re-probed on every refresh. */
|
|
4348
|
+
failedAt;
|
|
4349
|
+
constructor(options) {
|
|
4350
|
+
this.options = options;
|
|
4351
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
4352
|
+
this.now = options.now ?? Date.now;
|
|
4353
|
+
}
|
|
4354
|
+
/** Cached roster, without any network I/O. */
|
|
4355
|
+
cachedRoster() {
|
|
4356
|
+
return this.cached === void 0 ? void 0 : this.cached.roster;
|
|
4357
|
+
}
|
|
4358
|
+
async fetch(signal) {
|
|
4359
|
+
const now = this.now();
|
|
4360
|
+
if (this.cached !== void 0 && now - this.cached.at < ROSTER_CACHE_MS) return this.cached.roster;
|
|
4361
|
+
if (this.failedAt !== void 0 && now - this.failedAt < ROSTER_CACHE_MS) return void 0;
|
|
4362
|
+
try {
|
|
4363
|
+
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
4364
|
+
const region = regionOfCredential(credential);
|
|
4365
|
+
const base = this.options.baseUrl ?? (this.options.route === "enterprise" ? "https://console.enterprise.trae.cn" : REGION_GATEWAYS[region].chat);
|
|
4366
|
+
const response = await this.fetchImpl(traeEndpoint(base, TRAE_MODEL_DETAIL_PATH), {
|
|
4367
|
+
method: "POST",
|
|
4368
|
+
headers: buildTraeCnHeaders(credential, identity, { profile: "model-detail" }),
|
|
4369
|
+
body: JSON.stringify({
|
|
4370
|
+
...buildTraeModelDetailRequest(""),
|
|
4371
|
+
show_custom_model: true
|
|
4372
|
+
}),
|
|
4373
|
+
signal: signal ?? AbortSignal.timeout(ROSTER_TIMEOUT_MS)
|
|
4374
|
+
});
|
|
4375
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
4376
|
+
const roster = parseTraeOfficialRoster(await response.json());
|
|
4377
|
+
if (roster.size.configs === 0) throw new Error("roster document carried no config_name");
|
|
4378
|
+
this.cached = {
|
|
4379
|
+
at: now,
|
|
4380
|
+
roster
|
|
4381
|
+
};
|
|
4382
|
+
this.failedAt = void 0;
|
|
4383
|
+
return roster;
|
|
4384
|
+
} catch (error) {
|
|
4385
|
+
this.failedAt = now;
|
|
4386
|
+
this.options.log?.("dsh-connect-trae: IDE model roster unavailable, keeping every discovered row", { reason: error instanceof Error ? error.message : String(error) });
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
};
|
|
4391
|
+
/** Where the client asks for the roster; re-exported so diagnostics do not hard-code it. */
|
|
4392
|
+
const TRAE_OFFICIAL_ROSTER_PATH = TRAE_MODEL_DETAIL_PATH;
|
|
4393
|
+
//#endregion
|
|
3878
4394
|
//#region src/raw-chat.ts
|
|
3879
4395
|
const TRAE_RAW_CHAT_V2_PATH = "/api/ide/v2/llm_raw_chat";
|
|
3880
4396
|
const TRAE_RAW_CHAT_V1_PATH = "/api/ide/v1/llm_raw_chat";
|
|
@@ -4138,7 +4654,6 @@ function applyReasoningEffort(body, effort, capability) {
|
|
|
4138
4654
|
}
|
|
4139
4655
|
//#endregion
|
|
4140
4656
|
//#region src/solo.ts
|
|
4141
|
-
const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
4142
4657
|
/**
|
|
4143
4658
|
* `llm_utils_chat` functions to union per region, in priority order.
|
|
4144
4659
|
*
|
|
@@ -4202,22 +4717,12 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
|
4202
4717
|
* A model in NO chat function's roster is still left to the merge: it ships
|
|
4203
4718
|
* with no wire target and its call fails with a readable upstream message
|
|
4204
4719
|
* rather than being hidden (see mergeTraeModelSources).
|
|
4720
|
+
*
|
|
4721
|
+
* The list itself lives in {@link TRAE_CHANNEL_PRIORITY} (`model-order.ts`) and
|
|
4722
|
+
* is aliased here, not copied: the card groups its rows by these same names in
|
|
4723
|
+
* this same order.
|
|
4205
4724
|
*/
|
|
4206
|
-
const TRAE_DIRECTORY_FUNCTIONS =
|
|
4207
|
-
cn: [
|
|
4208
|
-
"solo_work_remote",
|
|
4209
|
-
TRAE_SOLO_FUNCTION,
|
|
4210
|
-
"solo_agent_remote",
|
|
4211
|
-
"chat_v3",
|
|
4212
|
-
"solo_coder"
|
|
4213
|
-
],
|
|
4214
|
-
ai: [
|
|
4215
|
-
"solo_work_remote",
|
|
4216
|
-
TRAE_SOLO_FUNCTION,
|
|
4217
|
-
"solo_agent_remote",
|
|
4218
|
-
"chat_v3"
|
|
4219
|
-
]
|
|
4220
|
-
};
|
|
4725
|
+
const TRAE_DIRECTORY_FUNCTIONS = TRAE_CHANNEL_PRIORITY;
|
|
4221
4726
|
const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
|
|
4222
4727
|
const TRAE_SOLO_MODELS_PATH = "/api/ide/v1/get_detail_param";
|
|
4223
4728
|
function classify(status) {
|
|
@@ -8822,24 +9327,28 @@ async function writeTraePoolProbes(panel, updates) {
|
|
|
8822
9327
|
* member, and leaving it in the file would resurrect a stale `rate-limited` row if
|
|
8823
9328
|
* the account were added back later. Written through the same lock/atomic path so a
|
|
8824
9329
|
* removal cannot interleave with a batch.
|
|
9330
|
+
*
|
|
9331
|
+
* The READ also sits inside the lock, like {@link writeTraePoolProbes}'s: a
|
|
9332
|
+
* delete is a read-modify-write, and a read taken outside the lock snapshots a
|
|
9333
|
+
* moment that a concurrent batch's write can then be older than — the delete
|
|
9334
|
+
* would write that stale snapshot back and silently discard the measurements
|
|
9335
|
+
* the batch had just learned.
|
|
8825
9336
|
*/
|
|
8826
9337
|
async function dropTraePoolProbe(panel, accountId) {
|
|
8827
|
-
const existing = await readTraePoolProbes(panel);
|
|
8828
|
-
if (existing[accountId] === void 0) return;
|
|
8829
9338
|
const path = traePoolStorePath(panel);
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
9339
|
+
await withFileLock(path, async () => {
|
|
9340
|
+
const existing = await readTraePoolProbes(panel);
|
|
9341
|
+
if (existing[accountId] === void 0) return;
|
|
9342
|
+
const probes = {};
|
|
9343
|
+
for (const [id, probe] of Object.entries(existing)) if (id !== accountId) probes[id] = probe;
|
|
9344
|
+
await writeFileAtomic(path, `${JSON.stringify({
|
|
9345
|
+
version: STORE_FORMAT_VERSION,
|
|
9346
|
+
probes
|
|
9347
|
+
}, null, 2)}\n`, {
|
|
9348
|
+
mode: 384,
|
|
9349
|
+
dirMode: 448
|
|
8841
9350
|
});
|
|
8842
|
-
}
|
|
9351
|
+
}).catch(() => {});
|
|
8843
9352
|
}
|
|
8844
9353
|
//#endregion
|
|
8845
9354
|
//#region src/model-config.ts
|
|
@@ -8898,48 +9407,6 @@ function parseTraeModelExtraConfigLogLine(line) {
|
|
|
8898
9407
|
}
|
|
8899
9408
|
}
|
|
8900
9409
|
//#endregion
|
|
8901
|
-
//#region src/model-detail.ts
|
|
8902
|
-
const TRAE_MODEL_DETAIL_PATH = "/api/ide/v1/batch_get_detail_param";
|
|
8903
|
-
const TRAE_MODEL_DETAIL_FUNCTIONS = [
|
|
8904
|
-
"ui_builder_v2",
|
|
8905
|
-
"solo_coder",
|
|
8906
|
-
"chat_v3",
|
|
8907
|
-
"solo_builder",
|
|
8908
|
-
"builder_v3",
|
|
8909
|
-
"builder",
|
|
8910
|
-
"chat",
|
|
8911
|
-
"inline_chat",
|
|
8912
|
-
"git_ai",
|
|
8913
|
-
"custom_agent_generation",
|
|
8914
|
-
"utils",
|
|
8915
|
-
"code_reviewer",
|
|
8916
|
-
"code_review_summary",
|
|
8917
|
-
"solo_agent",
|
|
8918
|
-
"solo_agent_remote",
|
|
8919
|
-
"solo_work_remote",
|
|
8920
|
-
"solo_agent_lite",
|
|
8921
|
-
"solo_work_lite",
|
|
8922
|
-
"solo_design_lite",
|
|
8923
|
-
"solo_design_remote",
|
|
8924
|
-
"multimodal",
|
|
8925
|
-
"system_diagnosis"
|
|
8926
|
-
];
|
|
8927
|
-
/** Build the exact request shape observed in successful current Trae CN logs. */
|
|
8928
|
-
function buildTraeModelDetailRequest(configName = "", custom = false) {
|
|
8929
|
-
return {
|
|
8930
|
-
functions: [...TRAE_MODEL_DETAIL_FUNCTIONS],
|
|
8931
|
-
agent_type: configName === "" ? "" : "solo_agent",
|
|
8932
|
-
current_config_info: {
|
|
8933
|
-
config_name: configName,
|
|
8934
|
-
is_custom_model: custom
|
|
8935
|
-
},
|
|
8936
|
-
mode_type: 0,
|
|
8937
|
-
access_type: 0,
|
|
8938
|
-
ab_force_vids: "",
|
|
8939
|
-
ab_autotest_advanced_mode: 0
|
|
8940
|
-
};
|
|
8941
|
-
}
|
|
8942
|
-
//#endregion
|
|
8943
9410
|
//#region src/redact.ts
|
|
8944
9411
|
/**
|
|
8945
9412
|
* Redaction helpers for diagnostics that a user may paste into a public issue.
|
|
@@ -9421,6 +9888,24 @@ function apply(ctx, config) {
|
|
|
9421
9888
|
credential: () => store.resolve(),
|
|
9422
9889
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
9423
9890
|
});
|
|
9891
|
+
/**
|
|
9892
|
+
* The IDE's own roster: which models the vendor's picker shows, per
|
|
9893
|
+
* function (issue #52).
|
|
9894
|
+
*
|
|
9895
|
+
* It is a REFINEMENT of a successful discovery, never a dependency: every
|
|
9896
|
+
* failure path in {@link TraeOfficialRosterClient.fetch} collapses to
|
|
9897
|
+
* `undefined`, and `applyOfficialRoster` then keeps every row. That is why
|
|
9898
|
+
* this can be wired in without a setting — it only acts where every chat
|
|
9899
|
+
* function hides a config (retire it) or where the vendor shows it on a
|
|
9900
|
+
* DIFFERENT chat function than the one we guessed (re-pin). Both are the
|
|
9901
|
+
* upstream's own answers, and both leave anything ambiguous alone.
|
|
9902
|
+
*/
|
|
9903
|
+
const officialRoster = new TraeOfficialRosterClient({
|
|
9904
|
+
credential: () => store.resolve(),
|
|
9905
|
+
identity,
|
|
9906
|
+
...panel === "enterprise" ? { route: "enterprise" } : {},
|
|
9907
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
9908
|
+
});
|
|
9424
9909
|
const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
|
|
9425
9910
|
/**
|
|
9426
9911
|
* One client + bridge per provider id.
|
|
@@ -9572,25 +10057,52 @@ function apply(ctx, config) {
|
|
|
9572
10057
|
};
|
|
9573
10058
|
},
|
|
9574
10059
|
discoverModels: async (signal) => {
|
|
9575
|
-
const
|
|
10060
|
+
const remote = await withDirectoryRetry(() => remoteCatalog.fetchModels(signal));
|
|
10061
|
+
const [wireModels, roster] = await Promise.all([solo.fetchModels(signal), officialRoster.fetch(signal)]);
|
|
10062
|
+
const merged = mergeTraeModelSources(remote, wireModels);
|
|
9576
10063
|
const callable = withChatCallableFunctions(merged, TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
9577
10064
|
const chatFunctions = TRAE_DIRECTORY_FUNCTIONS[region];
|
|
9578
10065
|
for (const model of merged) {
|
|
9579
10066
|
if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) continue;
|
|
9580
10067
|
ctx.logger.warn(`model directory row is pinned to a non-chat function and will be called through the default chat function instead: ${model.id} (pinned to function ${model.wireFunction})`);
|
|
9581
10068
|
}
|
|
10069
|
+
/**
|
|
10070
|
+
* Retire what no chat function offers any more, and re-pin what the
|
|
10071
|
+
* vendor offers under a DIFFERENT channel (issue #52).
|
|
10072
|
+
*
|
|
10073
|
+
* Runs AFTER `withChatCallableFunctions` on purpose: that step decides
|
|
10074
|
+
* the `wireFunction` this filter judges the row on. Rows the IDE roster
|
|
10075
|
+
* does not mention, and every row when the roster could not be read at
|
|
10076
|
+
* all, survive untouched — see `applyOfficialRoster`.
|
|
10077
|
+
*/
|
|
10078
|
+
const official = applyOfficialRoster(callable, roster, chatFunctions);
|
|
10079
|
+
/**
|
|
10080
|
+
* Fix the ORDER once, here, so it is a property of the catalog rather
|
|
10081
|
+
* than of the screen that happens to draw it (issue #52, P0).
|
|
10082
|
+
*
|
|
10083
|
+
* The merge walks twelve directory groups and `solo-remote.ts` records
|
|
10084
|
+
* that the group order varies between calls, so the same account could
|
|
10085
|
+
* get two different lists from two refreshes. Sorting by (channel
|
|
10086
|
+
* priority, name) makes the saved `lastCatalog` — and therefore the DSH
|
|
10087
|
+
* model picker, which renders in catalog order — identical across
|
|
10088
|
+
* refreshes. The card re-applies the same function for catalogs saved
|
|
10089
|
+
* before this existed.
|
|
10090
|
+
*/
|
|
10091
|
+
const visible = orderTraeModels(official.rows, region);
|
|
10092
|
+
if (official.hidden.length > 0) ctx.logger.warn(`hiding ${official.hidden.length} model(s) no chat function offers any more (issue #52)`, official.hidden);
|
|
10093
|
+
if (official.repinned.length > 0) ctx.logger.warn(`re-pinning ${official.repinned.length} model(s) to the function their vendor shows them on (issue #52)`, official.repinned);
|
|
9582
10094
|
wire.callableKeys.clear();
|
|
9583
|
-
for (const model of
|
|
10095
|
+
for (const model of visible) {
|
|
9584
10096
|
wire.callableKeys.add(model.id.trim().toLowerCase());
|
|
9585
10097
|
wire.callableKeys.add(model.name.trim().toLowerCase());
|
|
9586
10098
|
}
|
|
9587
|
-
wire.resolved =
|
|
9588
|
-
const wireTargets = wireTargetsOf(
|
|
10099
|
+
wire.resolved = visible.length > 0;
|
|
10100
|
+
const wireTargets = wireTargetsOf(visible);
|
|
9589
10101
|
wire.byId = wireTargets.byId;
|
|
9590
10102
|
wire.byName = wireTargets.byName;
|
|
9591
10103
|
latestDiscovery.length = 0;
|
|
9592
|
-
latestDiscovery.push(...
|
|
9593
|
-
return
|
|
10104
|
+
latestDiscovery.push(...visible);
|
|
10105
|
+
return visible;
|
|
9594
10106
|
},
|
|
9595
10107
|
/**
|
|
9596
10108
|
* One-click availability test. The ids come from the card, so each one is
|
|
@@ -10294,4 +10806,4 @@ function apply(ctx, config) {
|
|
|
10294
10806
|
});
|
|
10295
10807
|
}
|
|
10296
10808
|
//#endregion
|
|
10297
|
-
export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withContextAdaptation, withTraeRegion };
|
|
10809
|
+
export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHANNEL_PRIORITY, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_COMMON_FUNCTIONS, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_OFFICIAL_ROSTER_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeOfficialRosterClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyOfficialRoster, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, filterTraeModels, generationOf, groupTraeModels, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, officialVerdictOf, orderTraeModels, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeOfficialRoster, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, scopeTraeModels, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelCallChannel, traeModelDisplayName, traeModelInView, traeModelMatchesQuery, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withChatCallableFunctions, withContextAdaptation, withTraeRegion };
|