dsh-connect-trae 2.4.0 → 2.7.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/lib/index.js CHANGED
@@ -185,12 +185,22 @@ function displayKey(name) {
185
185
  * multiplier, reasoning and multimodal flags. `wire` (from `get_detail_param`)
186
186
  * supplies the real `llm_utils_chat` `config_name` — the only id the chat
187
187
  * endpoint actually accepts — plus the authoritative post-discount credit
188
- * multiplier when `display_contact_config` carries one. A remote row is only
189
- * callable when it maps to a wire `config_name`, so a remote row with no wire
190
- * match is DROPPED (it would otherwise be sent as an invalid `config_name` and
191
- * rejected with 4001 "param is invalid"). Verified 2026-08-30: the Remote
192
- * directory advertises `Doubao-Seed-Code` and `glm-5.3`, neither of which is a
193
- * current `config_name`; both fail every request, so they must not be exposed.
188
+ * multiplier when `display_contact_config` carries one.
189
+ *
190
+ * A remote row with no wire match is STILL KEPT (decision 2026-10-02, issue
191
+ * #19). It previously was dropped, on the reasoning that a row with no
192
+ * `config_name` is sent as an invalid one and rejected with 4001. That
193
+ * reasoning holds only for the accounts and functions we can measure, and
194
+ * measuring is the problem: whether a roster contains a model is ACCOUNT-
195
+ * SCOPED, so "no wire entry on a free account" cannot prove a paying account
196
+ * cannot call it. Dropping on that evidence silently denied those users models
197
+ * their own Trae IDE offers. The row therefore ships, and its call fails
198
+ * honestly: the bridge now translates the in-stream refusal into a readable
199
+ * error (`4001` → "Trae does not serve this model under the SOLO function the
200
+ * request used", `4011` → "Trae refused this model under the SOLO function the
201
+ * request used"), so a user who picks an unusable model is told why instead of
202
+ * seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
203
+ * which later turned out to be callable under `chat_v3`.
194
204
  *
195
205
  * Credit multiplier precedence: the wire's `display_contact_config` rate wins
196
206
  * whenever present (it is the post-discount figure the Trae IDE renders — the
@@ -217,8 +227,7 @@ function mergeTraeModelSources(remote, wire) {
217
227
  const result = [];
218
228
  for (const model of remote) {
219
229
  const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
220
- if (wireModel === void 0) continue;
221
- const creditMultiplier = wireModel.creditMultiplier ?? model.creditMultiplier;
230
+ const creditMultiplier = wireModel?.creditMultiplier ?? model.creditMultiplier;
222
231
  result.push({
223
232
  id: model.id,
224
233
  name: model.name,
@@ -232,8 +241,8 @@ function mergeTraeModelSources(remote, wire) {
232
241
  reasoning: model.reasoning,
233
242
  reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
234
243
  },
235
- ...wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
236
- ...wireModel.function === void 0 ? {} : { wireFunction: wireModel.function }
244
+ ...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
245
+ ...wireModel?.function === void 0 ? {} : { wireFunction: wireModel.function }
237
246
  });
238
247
  }
239
248
  return result;
@@ -1942,7 +1951,7 @@ function applyReasoningEffort(body, effort, capability) {
1942
1951
  //#region src/solo.ts
1943
1952
  const TRAE_SOLO_FUNCTION = "solo_work_lite";
1944
1953
  /**
1945
- * Directory functions to union per region, in priority order.
1954
+ * `llm_utils_chat` functions to union per region, in priority order.
1946
1955
  *
1947
1956
  * Trae spreads its callable roster across several SOLO-mode functions, and a
1948
1957
  * model is only usable through the one that lists it: `glm-5.3` is absent from
@@ -1950,14 +1959,69 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
1950
1959
  * calling it through the former answers `4001 param is invalid`, through the
1951
1960
  * latter streams normally). Rather than betting on a single function, the
1952
1961
  * directory unions them; the first function to provide a config wins, so the
1953
- * order below decides which wire name a model is called with.
1962
+ * order below decides which wire name AND which function a model is called with.
1963
+ *
1964
+ * THIS LIST IS NOT "directory functions" — it is the set of functions
1965
+ * `llm_utils_chat` actually accepts, because ownership here is what the chat
1966
+ * call replays. `solo_agent` is deliberately ABSENT (measured 2026-10-01 on a
1967
+ * live SG credential, issue #19): it is a Remote-agent roster function, not a
1968
+ * chat function. Every model sent with `function: "solo_agent"` — `gpt-5.4`,
1969
+ * `gpt-5.6-sol`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code` — answered
1970
+ * `event:error {"code":4011}` (message says "rate limit", it is really "not
1971
+ * served here"), while the same model with the same body answered normally
1972
+ * under `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`.
1973
+ *
1974
+ * Note `get_detail_param` DOES answer `solo_agent` with a 43-entry list, so
1975
+ * asking it looks helpful while making 8 of the 10 exposed ai-region models
1976
+ * uncallable: it won precedence and stamped its own name onto configs it
1977
+ * cannot serve. `solo_agent_remote` is the chat-callable half of that roster
1978
+ * (it answers `minimax-m3`, `gemini-3.1-pro`, … that no `solo_work_*` lists)
1979
+ * and therefore takes the last slot — after the two IDE functions that serve
1980
+ * their shared configs without the Remote plan gate.
1981
+ *
1982
+ * `chat_v3` is the widest chat roster of all (measured 2026-10-01 on live
1983
+ * credentials, issue #19 follow-up): its `get_detail_param` answers 40
1984
+ * config_names on `ai` — more than any other function except `solo_agent` (43,
1985
+ * which cannot serve chat). It lists models NO other chat function names, and
1986
+ * it is joined LAST so it only fills gaps: a config two functions list keeps
1987
+ * the earlier function, so no precedence is disturbed. The ai models it unlocks
1988
+ * were each verified callable end-to-end through the plugin's own
1989
+ * `prepareSoloBody` envelope:
1990
+ *
1991
+ * ai (+6): kimi-k2.7-code, deepseek-v4-flash-0731, deepseek-v3.2,
1992
+ * gemini-3-flash-premium, gemini_2.5_flash_premium, Dola-Seed-2.0-Code
1993
+ *
1994
+ * CN joins `chat_v3` and `solo_coder` too (2026-10-02). Both are callable there
1995
+ * — verified per model through `prepareSoloBody`, calling each model under a
1996
+ * function whose roster names it: `glm-5` / `glm-5.1` / `qwen-3.5` and
1997
+ * `deepseek-v4-pro` / `deepseek-v4-flash` under `solo_coder`, and
1998
+ * `doubao-seed-2.0-code` under `solo_coder` / `solo_work_remote` /
1999
+ * `solo_work_lite`. An earlier note here claimed those three answered `4001`
2000
+ * "under every function" — that was a measurement error, not an upstream
2001
+ * refusal: the probe had called them by display id while the chat endpoint needs
2002
+ * the config_name its owning function returned.
2003
+ *
2004
+ * This also corrects an older note that `Doubao-Seed-Code` "has no config_name
2005
+ * and must stay hidden" (docs/MODEL_MANAGEMENT_DESIGN.md): it does have one,
2006
+ * under `chat_v3`.
2007
+ *
2008
+ * A model in NO chat function's roster is still left to the merge: it ships
2009
+ * with no wire target and its call fails with a readable upstream message
2010
+ * rather than being hidden (see mergeTraeModelSources).
1954
2011
  */
1955
2012
  const TRAE_DIRECTORY_FUNCTIONS = {
1956
- cn: ["solo_work_remote", TRAE_SOLO_FUNCTION],
2013
+ cn: [
2014
+ "solo_work_remote",
2015
+ TRAE_SOLO_FUNCTION,
2016
+ "solo_agent_remote",
2017
+ "chat_v3",
2018
+ "solo_coder"
2019
+ ],
1957
2020
  ai: [
1958
- "solo_agent",
1959
2021
  "solo_work_remote",
1960
- TRAE_SOLO_FUNCTION
2022
+ TRAE_SOLO_FUNCTION,
2023
+ "solo_agent_remote",
2024
+ "chat_v3"
1961
2025
  ]
1962
2026
  };
1963
2027
  const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
@@ -2304,6 +2368,75 @@ function decodeTraeEvent(event) {
2304
2368
  }
2305
2369
  //#endregion
2306
2370
  //#region src/solo-bridge.ts
2371
+ /**
2372
+ * What Trae's in-stream error codes mean. Measured 2026-10-01 on a live SG
2373
+ * credential (issue #19), by sending the same body to several functions:
2374
+ *
2375
+ * - `1005` — the model needs a paid plan. Trae answers HTTP 200 with
2376
+ * `event:error {"code":1005,"message":"","extra":"{\"plan\":1}"}`, so the
2377
+ * gate is only visible in `extra.plan`; `gpt-6-sol` returned it under both
2378
+ * `solo_work_remote` and `solo_agent_remote` on a free account.
2379
+ * - `4011` — the function the request named does not serve that model. Its
2380
+ * message claims "exceeded the rate limit", which is misleading: the same
2381
+ * model answers normally when sent under `solo_work_remote`.
2382
+ * - `4001` — `config_name` is not one this function serves.
2383
+ */
2384
+ const TRAE_ERROR_HINTS = {
2385
+ 1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
2386
+ 4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
2387
+ 4011: "Trae refused this model under the SOLO function the request used"
2388
+ };
2389
+ /** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
2390
+ function traeErrorPlan(extra) {
2391
+ let value = extra;
2392
+ if (typeof value === "string") try {
2393
+ value = JSON.parse(value);
2394
+ } catch {
2395
+ return;
2396
+ }
2397
+ if (typeof value !== "object" || value === null) return void 0;
2398
+ const plan = value["plan"];
2399
+ return typeof plan === "number" ? plan : void 0;
2400
+ }
2401
+ /** A readable message for an in-stream Trae failure, with its code preserved. */
2402
+ function describeTraeFailure(failure) {
2403
+ const plan = traeErrorPlan(failure.extra);
2404
+ const parts = [
2405
+ failure.code === void 0 ? "Trae refused the request" : TRAE_ERROR_HINTS[failure.code] ?? "Trae refused the request",
2406
+ ...plan === void 0 ? [] : [`plan ${plan}`],
2407
+ ...failure.code === void 0 ? [] : [`Trae code ${failure.code}`]
2408
+ ];
2409
+ const detail = failure.message.trim();
2410
+ return detail === "" ? parts.join(" · ") : `${parts.join(" · ")} · upstream: ${detail}`;
2411
+ }
2412
+ /**
2413
+ * HTTP status + error kind for an in-stream failure, so the loopback shim can
2414
+ * answer with a real error instead of a truncated 200 stream.
2415
+ *
2416
+ * `1005` is a subscription gate: 402 is what makes clients stop rather than
2417
+ * retry, and it is truthful. Everything else is a client-side rejection.
2418
+ */
2419
+ function classifyTraeFailure(failure) {
2420
+ return failure.code === 1005 ? {
2421
+ status: 402,
2422
+ kind: "hard_credit"
2423
+ } : {
2424
+ status: 400,
2425
+ kind: "client"
2426
+ };
2427
+ }
2428
+ /** Recognise an in-stream failure event, or return undefined for anything else. */
2429
+ function traeFailureOf(decoded) {
2430
+ if (decoded.type !== "unknown") return void 0;
2431
+ const payload = decoded.data;
2432
+ const code = typeof payload?.["code"] === "number" ? payload["code"] : void 0;
2433
+ if (decoded.event !== "error" && !(code !== void 0 && code >= 4e3)) return void 0;
2434
+ return {
2435
+ ...code === void 0 ? {} : { code },
2436
+ message: typeof payload?.["message"] === "string" ? payload["message"] : "",
2437
+ ...payload?.["extra"] === void 0 ? {} : { extra: payload["extra"] }
2438
+ };
2439
+ }
2307
2440
  function normalizeToolCalls(value) {
2308
2441
  if (!Array.isArray(value)) return [];
2309
2442
  const calls = [];
@@ -2328,6 +2461,10 @@ function normalizeToolCalls(value) {
2328
2461
  function bridgeTraeSoloStream(response, model) {
2329
2462
  const source = response.body;
2330
2463
  if (source === null) return new Response(null, { status: 502 });
2464
+ return bridgeTraeSource(source, model);
2465
+ }
2466
+ /** {@link bridgeTraeSoloStream} over an already-opened byte stream. */
2467
+ function bridgeTraeSource(source, model) {
2331
2468
  const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
2332
2469
  const created = Math.floor(Date.now() / 1e3);
2333
2470
  const decoder = new TextDecoder();
@@ -2356,9 +2493,8 @@ function bridgeTraeSoloStream(response, model) {
2356
2493
  const consume = (event) => {
2357
2494
  const decoded = decodeTraeEvent(event);
2358
2495
  if (decoded.type === "unknown") {
2359
- const payload = decoded.data;
2360
- const code = typeof payload?.["code"] === "number" ? payload["code"] : void 0;
2361
- 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 ?? "?"})`);
2496
+ const failure = traeFailureOf(decoded);
2497
+ if (failure !== void 0) upstreamError = new Error(describeTraeFailure(failure));
2362
2498
  return;
2363
2499
  }
2364
2500
  if (decoded.type === "delta") {
@@ -2428,6 +2564,123 @@ function bridgeTraeSoloStream(response, model) {
2428
2564
  headers: { "Content-Type": "text/event-stream" }
2429
2565
  });
2430
2566
  }
2567
+ /**
2568
+ * How far {@link openTraeSource} reads before giving up on finding a failure.
2569
+ * Trae opens a real answer with small `metadata` / `timing_cost` events, so a
2570
+ * healthy stream is recognised well inside these bounds; they exist only so a
2571
+ * pathological stream cannot stall the response headers.
2572
+ */
2573
+ const PEEK_MAX_CHUNKS = 64;
2574
+ const PEEK_MAX_BYTES = 262144;
2575
+ /** Replay the bytes the peek consumed, then drain the rest of the upstream. */
2576
+ function replayTraeSource(buffered, reader, ended) {
2577
+ let index = 0;
2578
+ return new ReadableStream({
2579
+ async pull(controller) {
2580
+ const chunk = buffered[index];
2581
+ if (chunk !== void 0) {
2582
+ index += 1;
2583
+ controller.enqueue(chunk);
2584
+ return;
2585
+ }
2586
+ if (ended) {
2587
+ controller.close();
2588
+ return;
2589
+ }
2590
+ const next = await reader.read();
2591
+ if (next.done) {
2592
+ controller.close();
2593
+ return;
2594
+ }
2595
+ controller.enqueue(next.value);
2596
+ },
2597
+ async cancel(reason) {
2598
+ await reader.cancel(reason).catch(() => {});
2599
+ }
2600
+ });
2601
+ }
2602
+ /**
2603
+ * Open the upstream answer, failing fast when Trae refuses BEFORE producing any
2604
+ * output.
2605
+ *
2606
+ * This exists because Trae reports authorisation and subscription failures
2607
+ * inside a **200** SSE body. Relaying that as a 200 and aborting mid-stream
2608
+ * (what this bridge used to do) loses the code at the HTTP boundary: pi-ai only
2609
+ * sees a truncated stream and reports `Stream ended without finish_reason`,
2610
+ * which reads as a network fault and is retried five times — the exact symptom
2611
+ * in issue #19. Peeking until the first real output lets a refusal become a
2612
+ * normal error response with its code and plan intact.
2613
+ *
2614
+ * The peek is limited to the head of the stream and replayed verbatim, so a
2615
+ * healthy answer keeps streaming with no buffering beyond the first event.
2616
+ */
2617
+ async function openTraeSource(response) {
2618
+ const body = response.body;
2619
+ if (body === null) return {
2620
+ ok: false,
2621
+ status: 502,
2622
+ kind: "server",
2623
+ message: "Trae upstream returned no response body"
2624
+ };
2625
+ const reader = body.getReader();
2626
+ const decoder = new TextDecoder();
2627
+ const sse = new SseDecoder();
2628
+ const buffered = [];
2629
+ let bytes = 0;
2630
+ let ended = false;
2631
+ try {
2632
+ while (buffered.length < PEEK_MAX_CHUNKS && bytes < PEEK_MAX_BYTES) {
2633
+ const next = await reader.read();
2634
+ if (next.done) {
2635
+ ended = true;
2636
+ break;
2637
+ }
2638
+ buffered.push(next.value);
2639
+ bytes += next.value.byteLength;
2640
+ let answered = false;
2641
+ for (const event of sse.push(decoder.decode(next.value, { stream: true }))) {
2642
+ const decoded = decodeTraeEvent(event);
2643
+ const failure = traeFailureOf(decoded);
2644
+ if (failure !== void 0) {
2645
+ await reader.cancel().catch(() => {});
2646
+ return {
2647
+ ok: false,
2648
+ ...classifyTraeFailure(failure),
2649
+ message: describeTraeFailure(failure)
2650
+ };
2651
+ }
2652
+ if (decoded.type !== "unknown") answered = true;
2653
+ }
2654
+ if (answered) break;
2655
+ }
2656
+ } catch (error) {
2657
+ reader.releaseLock();
2658
+ return {
2659
+ ok: false,
2660
+ status: 0,
2661
+ kind: "server",
2662
+ message: `transport error: ${String(error)}`
2663
+ };
2664
+ }
2665
+ return {
2666
+ ok: true,
2667
+ source: replayTraeSource(buffered, reader, ended)
2668
+ };
2669
+ }
2670
+ /**
2671
+ * Bridge an upstream answer, turning an early refusal into a real error result.
2672
+ *
2673
+ * The sibling {@link bridgeTraeSoloStream} stays synchronous and always answers
2674
+ * 200; it is the primitive, and this is the policy the bridge class uses.
2675
+ */
2676
+ async function bridgeTraeSoloStreamOrFail(response, model) {
2677
+ const opened = await openTraeSource(response);
2678
+ if (!opened.ok) return opened;
2679
+ return {
2680
+ ok: true,
2681
+ response: bridgeTraeSource(opened.source, model)
2682
+ };
2683
+ }
2431
2684
  /** Native SOLO client wrapper used by the loopback OpenAI adapter. */
2432
2685
  var TraeSoloBridge = class {
2433
2686
  upstream;
@@ -2475,10 +2728,7 @@ var TraeSoloBridge = class {
2475
2728
  }
2476
2729
  const result = await this.upstream.chatStream(prepared, signal);
2477
2730
  if (!result.ok) return result;
2478
- return {
2479
- ok: true,
2480
- response: bridgeTraeSoloStream(result.response, model)
2481
- };
2731
+ return await bridgeTraeSoloStreamOrFail(result.response, model);
2482
2732
  }
2483
2733
  };
2484
2734
  //#endregion
@@ -2541,6 +2791,66 @@ function parseTraeRemoteModel(value) {
2541
2791
  //#region src/solo-remote.ts
2542
2792
  const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
2543
2793
  /**
2794
+ * Remote-directory functions to ask for, per region.
2795
+ *
2796
+ * The remote `/models` answer is **grouped by function** and the groups are not
2797
+ * interchangeable (measured 2026-10-01, issue #19):
2798
+ *
2799
+ * ```
2800
+ * solo_agent : 19 models ← superset; also the ONLY source of
2801
+ * gpt-6-astra / gpt-5.6-sol|terra|luna /
2802
+ * glm-5.2 / gpt-5.5 / Seed-2.1-Turbo
2803
+ * solo_agent_remote : 10 models ← what this client used to read, and all it
2804
+ * read: the preferred group only
2805
+ * ```
2806
+ *
2807
+ * `solo_agent` ⊇ `solo_agent_remote`, so reading just the latter hid nine
2808
+ * models that the Trae IDE does show — exactly what the reporter saw. Both
2809
+ * groups are therefore requested, and every returned group is unioned.
2810
+ *
2811
+ * Asking for a function the gateway does not serve is harmless: unknown names
2812
+ * are ignored rather than rejected (`solo_work_remote` returns no group on
2813
+ * `ai`, and the request still answers HTTP 200).
2814
+ *
2815
+ * `chat_v3` was added on 2026-10-01 (issue #19 follow-up) because it carries
2816
+ * three ai-region models that appear in NO other group — `deepseek-v3.2`,
2817
+ * `gemini-3-flash-premium`, `gemini_2.5_flash_premium` — and those three are
2818
+ * callable through it (see TRAE_DIRECTORY_FUNCTIONS in solo.ts).
2819
+ *
2820
+ * Both regions now ask the SAME group list (2026-10-02). CN previously asked
2821
+ * only `solo_agent_remote` + `solo_work_remote`, which left its `solo_coder`
2822
+ * group (12 models) unread — the same shape of miss as the ai one. Every group
2823
+ * below was measured to exist on both gateways; a region that does not serve a
2824
+ * name simply returns no group for it, which the union tolerates. Reading costs
2825
+ * one query and the merge no longer discards rows for lacking a wire match (see
2826
+ * mergeTraeModelSources), so an unread group is a model silently missing from
2827
+ * the user's own list — the failure this list exists to prevent.
2828
+ *
2829
+ * `builder_v3` is included for the same reason even though neither region's
2830
+ * current wire lists name it: its rows are real directory entries, and whether
2831
+ * a paying account can call one is not something this client can determine.
2832
+ */
2833
+ const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
2834
+ cn: [
2835
+ "solo_agent",
2836
+ "solo_agent_remote",
2837
+ "solo_work_remote",
2838
+ "solo_work_lite",
2839
+ "chat_v3",
2840
+ "solo_coder",
2841
+ "builder_v3"
2842
+ ],
2843
+ ai: [
2844
+ "solo_agent",
2845
+ "solo_agent_remote",
2846
+ "solo_work_remote",
2847
+ "solo_work_lite",
2848
+ "chat_v3",
2849
+ "solo_coder",
2850
+ "builder_v3"
2851
+ ]
2852
+ };
2853
+ /**
2544
2854
  * Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
2545
2855
  * CN locale headers; the international directory lives on the shared
2546
2856
  * `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
@@ -2590,16 +2900,16 @@ var TraeSoloRemoteCatalogClient = class {
2590
2900
  const region = regionOfCredential(await this.options.credential());
2591
2901
  const base = this.baseUrl ?? REGION_GATEWAYS[region].remote;
2592
2902
  const headers = await this.headers(region);
2593
- const response = await this.fetchImpl(`${base}/models?functions=solo_agent_remote,solo_work_remote`, {
2903
+ const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
2904
+ const response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
2594
2905
  headers,
2595
2906
  signal: signal ?? AbortSignal.timeout(3e4)
2596
2907
  });
2597
2908
  if (!response.ok) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
2598
2909
  const groups = (await response.json()).data?.list ?? [];
2599
- const preferred = groups.find((group) => group.function === "solo_agent_remote") ?? groups[0];
2600
2910
  const seen = /* @__PURE__ */ new Set();
2601
2911
  const models = [];
2602
- for (const raw of preferred?.models ?? []) {
2912
+ for (const group of groups) for (const raw of group.models ?? []) {
2603
2913
  const model = parseTraeRemoteModel(raw);
2604
2914
  if (model === void 0 || seen.has(model.id)) continue;
2605
2915
  seen.add(model.id);
@@ -3145,6 +3455,165 @@ var TraeDelegatingUpstreamClient = class {
3145
3455
  }
3146
3456
  };
3147
3457
  //#endregion
3458
+ //#region src/model-probe.ts
3459
+ /**
3460
+ * How long one probe may take. Trae answers a one-word prompt in a few seconds;
3461
+ * beyond this the answer is not worth waiting for and is reported as unknown.
3462
+ */
3463
+ const TRAE_PROBE_TIMEOUT_MS = 3e4;
3464
+ /** Upstream error codes the probe recognises, and what they mean. */
3465
+ const GATE_CODES = /* @__PURE__ */ new Set([1005]);
3466
+ const UNSUPPORTED_CODES = /* @__PURE__ */ new Set([4001, 4011]);
3467
+ /** Pull `Trae code N` out of an already-translated failure message. */
3468
+ function codeOf(message) {
3469
+ const match = /Trae code (\d+)/u.exec(message);
3470
+ return match === null ? void 0 : Number(match[1]);
3471
+ }
3472
+ /**
3473
+ * Classify a failure that arrived as a thrown stream error rather than a
3474
+ * returned result.
3475
+ *
3476
+ * The bridge errors the stream (instead of returning a result) when a refusal
3477
+ * interrupts an answer that had already started; its message is the same
3478
+ * translated text, so a named code is still decisive. Anything without a
3479
+ * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
3480
+ */
3481
+ function classifyProbeError(message) {
3482
+ if (codeOf(message) !== void 0) {
3483
+ const classified = classifyProbeResult({
3484
+ ok: false,
3485
+ status: 0,
3486
+ kind: "client",
3487
+ message
3488
+ }, false);
3489
+ if (classified.verdict !== "unknown") return classified;
3490
+ }
3491
+ return {
3492
+ verdict: "unknown",
3493
+ detail: message === "" ? "the stream failed without a message" : `stream failed: ${message}`
3494
+ };
3495
+ }
3496
+ /**
3497
+ * Classify a completed upstream call.
3498
+ *
3499
+ * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
3500
+ * arrived inside a 200 body has already been turned into a real error result
3501
+ * (and its code translated into `message`) by the bridge — this function only
3502
+ * has to read it back.
3503
+ */
3504
+ function classifyProbeResult(result, outputSeen) {
3505
+ if (result.ok) return outputSeen ? {
3506
+ verdict: "available",
3507
+ detail: "upstream answered"
3508
+ } : {
3509
+ verdict: "unknown",
3510
+ detail: "the stream ended without any output"
3511
+ };
3512
+ const message = result.message.trim() === "" ? `HTTP ${result.status}` : result.message;
3513
+ if (result.kind === "authentication" || result.status === 401 || result.status === 403) return {
3514
+ verdict: "auth",
3515
+ detail: message
3516
+ };
3517
+ if (result.kind === "soft_rate" || result.status === 429) return {
3518
+ verdict: "rate_limited",
3519
+ detail: message
3520
+ };
3521
+ const code = codeOf(message);
3522
+ if (code !== void 0 && GATE_CODES.has(code)) return {
3523
+ verdict: "gated",
3524
+ detail: message
3525
+ };
3526
+ if (code !== void 0 && UNSUPPORTED_CODES.has(code)) return {
3527
+ verdict: "unsupported",
3528
+ detail: message
3529
+ };
3530
+ if (result.kind === "client" && result.status >= 400 && result.status < 500) return {
3531
+ verdict: "unsupported",
3532
+ detail: message
3533
+ };
3534
+ return {
3535
+ verdict: "unknown",
3536
+ detail: message
3537
+ };
3538
+ }
3539
+ /** Read a probe response without letting a large or endless body stall. */
3540
+ async function readProbeOutput(response) {
3541
+ try {
3542
+ const text = await response.text();
3543
+ return { output: /"content"\s*:\s*"[^"]/u.test(text) || /"tool_calls"/u.test(text) };
3544
+ } catch (error) {
3545
+ return {
3546
+ output: false,
3547
+ error: error instanceof Error ? error : new Error(String(error))
3548
+ };
3549
+ }
3550
+ }
3551
+ /** The minimal chat call one probe makes. */
3552
+ function probeBody(model) {
3553
+ return JSON.stringify({
3554
+ model,
3555
+ messages: [{
3556
+ role: "user",
3557
+ content: "ping"
3558
+ }]
3559
+ });
3560
+ }
3561
+ /**
3562
+ * Probe one model through the bridge the plugin actually chats with, so the
3563
+ * recorded verdict describes the user's real path (same wire resolution, same
3564
+ * function stamping, same error translation).
3565
+ */
3566
+ async function probeOneModel(chat, id, timeoutMs = TRAE_PROBE_TIMEOUT_MS) {
3567
+ const controller = new AbortController();
3568
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3569
+ try {
3570
+ const result = await chat(probeBody(id), controller.signal);
3571
+ if (!result.ok) return {
3572
+ id,
3573
+ ...classifyProbeResult(result, false)
3574
+ };
3575
+ const { output, error } = await readProbeOutput(result.response);
3576
+ if (error !== void 0) return {
3577
+ id,
3578
+ ...classifyProbeError(error.message)
3579
+ };
3580
+ return {
3581
+ id,
3582
+ ...classifyProbeResult(result, output)
3583
+ };
3584
+ } catch (error) {
3585
+ if (controller.signal.aborted) return {
3586
+ id,
3587
+ verdict: "unknown",
3588
+ detail: `no answer within ${Math.round(timeoutMs / 1e3)}s`
3589
+ };
3590
+ return {
3591
+ id,
3592
+ ...classifyProbeError(error instanceof Error ? error.message : String(error))
3593
+ };
3594
+ } finally {
3595
+ clearTimeout(timer);
3596
+ }
3597
+ }
3598
+ /**
3599
+ * Probe a list of models ONE AT A TIME.
3600
+ *
3601
+ * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
3602
+ * would manufacture the very `rate_limited`/`unknown` verdicts this feature
3603
+ * must avoid. An authentication failure stops the run — every later probe would
3604
+ * fail the same way, and spending calls to learn that again is waste.
3605
+ */
3606
+ async function probeModelsSequentially(chat, ids, onResult) {
3607
+ const results = [];
3608
+ for (const [index, id] of ids.entries()) {
3609
+ const result = await probeOneModel(chat, id);
3610
+ results.push(result);
3611
+ onResult?.(result, index);
3612
+ if (result.verdict === "auth") break;
3613
+ }
3614
+ return results;
3615
+ }
3616
+ //#endregion
3148
3617
  //#region src/usage.ts
3149
3618
  /**
3150
3619
  * Read-only Trae usage/credits client.
@@ -3285,6 +3754,8 @@ var TraeUsageClient = class {
3285
3754
  const fissionStart = asNumber(payload["solo_fission_start_time"]);
3286
3755
  const fissionExpire = asNumber(payload["solo_fission_expire_time"]);
3287
3756
  const fissionMax = asNumber(payload["solo_fission_max_usage"]);
3757
+ const payIdentity = asNumber(payload["user_pay_identity"]);
3758
+ const payIdentityStr = typeof payload["user_pay_identity_str"] === "string" && payload["user_pay_identity_str"].trim() !== "" ? payload["user_pay_identity_str"].trim() : void 0;
3288
3759
  return {
3289
3760
  isDollarUsageBilling: flag("is_dollar_usage_billing"),
3290
3761
  hasPackage: flag("has_package"),
@@ -3295,6 +3766,8 @@ var TraeUsageClient = class {
3295
3766
  enableSoloBuilder: flag("enable_solo_builder"),
3296
3767
  enableSoloCoder: flag("enable_solo_coder"),
3297
3768
  enableSoloWeb: flag("enable_solo_web"),
3769
+ ...payIdentity === void 0 ? {} : { payIdentity },
3770
+ ...payIdentityStr === void 0 ? {} : { payIdentityStr },
3298
3771
  ...fissionStart === void 0 || fissionExpire === void 0 || fissionMax === void 0 ? {} : { fission: {
3299
3772
  startTimeMs: fissionStart,
3300
3773
  expireTimeMs: fissionExpire,
@@ -3404,6 +3877,13 @@ var TraeUsageClient = class {
3404
3877
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
3405
3878
  /** Plugin-owned live model refresh endpoint. */
3406
3879
  const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
3880
+ /**
3881
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
3882
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
3883
+ * (minimal) chat call per model — so it is user-initiated only, and the card
3884
+ * labels the button as consuming a small amount.
3885
+ */
3886
+ const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
3407
3887
  /** Plugin-owned local account rescan endpoint. */
3408
3888
  const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
3409
3889
  /**
@@ -3568,6 +4048,39 @@ function json(res, status, body) {
3568
4048
  });
3569
4049
  res.end(payload);
3570
4050
  }
4051
+ /**
4052
+ * Read the ids to test from a POST body.
4053
+ *
4054
+ * Only ids present in the region's live catalog are accepted downstream (see
4055
+ * index.ts), so this reader only has to produce a bounded, string-only list.
4056
+ */
4057
+ async function readModelTestBody(req) {
4058
+ const chunks = [];
4059
+ let size = 0;
4060
+ for await (const chunk of req) {
4061
+ const buffer = chunk;
4062
+ size += buffer.byteLength;
4063
+ if (size > 65536) throw new Error("request body too large");
4064
+ chunks.push(buffer);
4065
+ }
4066
+ const text = Buffer.concat(chunks).toString("utf8").trim();
4067
+ if (text === "") return [];
4068
+ let parsed;
4069
+ try {
4070
+ parsed = JSON.parse(text);
4071
+ } catch {
4072
+ throw new Error("invalid JSON body");
4073
+ }
4074
+ const raw = typeof parsed === "object" && parsed !== null ? parsed["ids"] : void 0;
4075
+ if (!Array.isArray(raw)) throw new Error("body must carry an ids array");
4076
+ const ids = [];
4077
+ for (const value of raw) {
4078
+ if (typeof value !== "string") continue;
4079
+ const id = value.trim();
4080
+ if (id !== "" && !ids.includes(id)) ids.push(id);
4081
+ }
4082
+ return ids;
4083
+ }
3571
4084
  /** Loopback browser origins only; other devices are refused until trusted origins exist. */
3572
4085
  function loopbackOrigin(req) {
3573
4086
  const origin = req.headers.origin;
@@ -3750,6 +4263,40 @@ function registerTraeUsageRoute(ctx, deps) {
3750
4263
  }
3751
4264
  });
3752
4265
  /**
4266
+ * One-click availability test for the enabled models.
4267
+ *
4268
+ * This ROUTE COSTS QUOTA: each tested model gets a real, minimal chat call.
4269
+ * Every other plugin surface is read-only, so the guards here are tighter
4270
+ * than a GET's — POST only, loopback origin only, and the id list is taken
4271
+ * from the request body and validated against the region's live catalog, so
4272
+ * a caller cannot spend calls on arbitrary upstream config names. The count
4273
+ * is capped for the same reason.
4274
+ */
4275
+ const disposeModelsTest = ctx.webServer.register({
4276
+ kind: "exact",
4277
+ path: TRAE_MODELS_TEST_PATH,
4278
+ handler: async (req, res) => {
4279
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4280
+ if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
4281
+ if (deps.probeModels === void 0) return json(res, 503, { error: "model test unavailable" });
4282
+ const region = requestRegion(req, res);
4283
+ if (region === void 0) return;
4284
+ let ids;
4285
+ try {
4286
+ ids = await readModelTestBody(req);
4287
+ } catch (error) {
4288
+ return json(res, 400, { error: safeMessage(error) });
4289
+ }
4290
+ if (ids.length === 0) return json(res, 400, { error: "no models to test" });
4291
+ if (ids.length > 64) return json(res, 400, { error: `too many models (limit ${String(64)})` });
4292
+ try {
4293
+ json(res, 200, await deps.probeModels(region, ids));
4294
+ } catch (error) {
4295
+ json(res, 500, { error: safeMessage(error) });
4296
+ }
4297
+ }
4298
+ });
4299
+ /**
3753
4300
  * Daily check-in claim — the ONLY route in this plugin that mutates
3754
4301
  * upstream account state, so it is guarded more tightly than its siblings:
3755
4302
  * POST only, loopback origin only, and the status read runs FIRST so a day
@@ -3815,6 +4362,7 @@ function registerTraeUsageRoute(ctx, deps) {
3815
4362
  return () => {
3816
4363
  disposeCheckin();
3817
4364
  disposeRefresh();
4365
+ disposeModelsTest();
3818
4366
  disposeAccounts();
3819
4367
  disposeUsage();
3820
4368
  };
@@ -4332,6 +4880,32 @@ function apply(ctx, config) {
4332
4880
  }
4333
4881
  return merged;
4334
4882
  },
4883
+ /**
4884
+ * One-click availability test. The ids come from the card, so each one is
4885
+ * checked against THIS region's live catalog first: a caller must not be
4886
+ * able to spend quota probing arbitrary upstream config names, and a row the
4887
+ * region does not know is reported as skipped rather than silently dropped.
4888
+ *
4889
+ * The probe goes through `upstream` — the same bridge real chats use — so
4890
+ * wire resolution, function stamping and error translation all match what
4891
+ * the user gets when they actually send a message.
4892
+ */
4893
+ probeModels: async (ids) => {
4894
+ const known = /* @__PURE__ */ new Set();
4895
+ for (const model of [...catalog.current(), ...displayModels(current(), region)]) {
4896
+ known.add(model.id.trim().toLowerCase());
4897
+ known.add(model.name.trim().toLowerCase());
4898
+ }
4899
+ const testable = [];
4900
+ const skipped = [];
4901
+ for (const id of ids) if (known.has(id.trim().toLowerCase())) testable.push(id);
4902
+ else skipped.push(id);
4903
+ const results = await probeModelsSequentially((body, signal) => upstream.chatStream(body, signal), testable);
4904
+ return skipped.length === 0 ? { results } : {
4905
+ results,
4906
+ skipped
4907
+ };
4908
+ },
4335
4909
  invalidateAdapter: () => {},
4336
4910
  rawDiagnostic: () => rawDiagnostic()
4337
4911
  };
@@ -4343,6 +4917,7 @@ function apply(ctx, config) {
4343
4917
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
4344
4918
  regionEnabled: (region) => regionEnabled(current(), region),
4345
4919
  discoverModels: (region, signal) => stacks[region].discoverModels(signal),
4920
+ probeModels: (region, ids) => stacks[region].probeModels(ids),
4346
4921
  rawDiagnostic: (region) => stacks[region].rawDiagnostic()
4347
4922
  }));
4348
4923
  /**
@@ -4487,4 +5062,4 @@ function apply(ctx, config) {
4487
5062
  });
4488
5063
  }
4489
5064
  //#endregion
4490
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_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, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
5065
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };