dsh-connect-trae 3.7.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -61,6 +61,29 @@ declare function traeStorageCandidates(platform?: NodeJS.Platform, home?: string
61
61
  //#region src/region.d.ts
62
62
  /** Storage/routing bucket: the CN service or the international (ai) service. */
63
63
  type TraeRegion = 'cn' | 'ai';
64
+ /**
65
+ * One section of the plugin card: a provider the user can switch, bind an
66
+ * account to, and pick models for, INDEPENDENTLY.
67
+ *
68
+ * A panel is not a region. `cn` and `ai` happen to be one-to-one with regions,
69
+ * but the CN enterprise pool shares the `cn` credential while billing a
70
+ * different account and serving a different roster — it needs its own switch,
71
+ * its own account binding and its own model selection, which is exactly what a
72
+ * panel is. Keeping panels a separate concept from regions is what lets
73
+ * `enterprise` be handled by the same settings plumbing with no new storage
74
+ * shape: each panel is just one more slot in the `regions` map.
75
+ */
76
+ type TraePanel = 'cn' | 'ai' | 'enterprise';
77
+ /** Every panel, in card tab order. */
78
+ declare const TRAE_PANELS: readonly TraePanel[];
79
+ /** The region whose credential/account pool a panel draws on. */
80
+ declare function regionOfPanel(panel: TraePanel): TraeRegion;
81
+ /** The provider id a panel publishes. */
82
+ declare function providerOfPanel(panel: TraePanel): string;
83
+ /** The panel that owns a provider id, or undefined for an unknown id. */
84
+ declare function panelOfProvider(provider: string): TraePanel | undefined;
85
+ /** Narrow a stored settings key to a panel. */
86
+ declare function panelOfKey(value: unknown): TraePanel | undefined;
64
87
  /** One region's upstream gateway bases. */
65
88
  interface TraeRegionGateways {
66
89
  /** Chat + agent API base (the `llm_utils_chat` / raw-chat family). */
@@ -191,8 +214,15 @@ declare function traeInputModalities(model: Pick<TraeModelInfo, 'input'>): TraeI
191
214
  * (wire resolution, callable-key filtering) must keep matching the
192
215
  * undecorated name; only the model rows handed to DSH (adapter catalog and
193
216
  * model discovery) use this decorated name.
217
+ *
218
+ * `withRate` is false for the ENTERPRISE panel. The multiplier is a
219
+ * consumer-pool figure — personal credits per token, read from the consumer
220
+ * directory — and the tenant pool does not bill against it, so printing it in
221
+ * the model picker would price a model the tenant is not charged for. The rate
222
+ * is part of the NAME that reaches DSH, which is why this is a parameter here
223
+ * and not a rendering decision downstream.
194
224
  */
195
- declare function traeModelDisplayName(model: Pick<TraeModelInfo, 'name' | 'creditMultiplier'>): string;
225
+ declare function traeModelDisplayName(model: Pick<TraeModelInfo, 'name' | 'creditMultiplier'>, withRate?: boolean): string;
196
226
  /** Apply the user's explicit image opt-ins plus Trae's own multimodal flag. */
197
227
  declare function applyImageSelection(models: readonly TraeModelInfo[], selected: ReadonlySet<string>): TraeModelInfo[];
198
228
  /** One row from `get_detail_param`: the authoritative llm_utils_chat wire id + display name. */
@@ -296,167 +326,1036 @@ declare class TraeCatalog {
296
326
  set(models: readonly TraeModelInfo[]): void;
297
327
  }
298
328
  //#endregion
299
- //#region src/pi-ai-runtime.d.ts
329
+ //#region src/status-paths.d.ts
330
+ /** Plugin-owned usage endpoint consumed by its browser half. */
331
+ declare const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
300
332
  /**
301
- * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
302
- * context shape that copy consumes (issue #27).
303
- *
304
- * WHY THIS EXISTS. The plugin declares pi-ai as a peer, so the module it
305
- * imports is whatever the install provides — and the two generations consume
306
- * DIFFERENT context shapes:
307
- *
308
- * - **modern** (0.86+): every public entry point calls `normalizeContext()`
309
- * once, so what reaches a provider is a normalized *transcript*:
310
- * `{ messages: [ { role: 'system', content, toolsAdded }, … ] }`. The system
311
- * prompt and the tool declarations live ONLY on that leading system message;
312
- * the top-level `systemPrompt` / `tools` fields are gone, and the api never
313
- * reads them back.
314
- * - **legacy** (≤0.85): expects `{ systemPrompt, tools, messages }` and has no
315
- * branch for a `system` message inside `messages` at all. Its token estimator
316
- * crashes on one — `estimateMessageTokens` iterates the string `content`
317
- * character by character and reads `.name` off a character — and its wire
318
- * converter silently drops it.
319
- *
320
- * The mismatch is reachable in BOTH directions, because DSH itself spans both
321
- * generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
322
- * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`, and 0.2.1-rc.1 —
323
- * the current desktop host — declares **`^1.1.0`**, which also stopped reading
324
- * the top-level fields entirely), while this plugin accepts the whole range
325
- * (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer, the other one is wrong:
326
- *
327
- * | host | resolved api | outcome |
328
- * |---|---|---|
329
- * | legacy | legacy | fine |
330
- * | legacy | modern | no crash, but prompt + tools silently LOST |
331
- * | modern | legacy | CRASH: `Cannot read properties of undefined (reading 'length')` |
332
- * | modern | modern | fine |
333
- *
334
- * THE BOUNDARY IS 0.86, NOT 0.87. Checked against the published tarballs:
335
- * `dist/utils/transcript.js` — and with it `normalizeContext`, the
336
- * `role === 'system'` estimator branch, and the switch away from reading
337
- * `systemPrompt`/`tools` — first appears in **0.86.0**. A `>= 0.87` gate
338
- * misclassifies 0.86.x as legacy and folds a context the modern api consumes
339
- * natively, which deletes the only carrier of the prompt and the tools.
333
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
334
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
335
+ * (minimal) chat call per model — so it is user-initiated only, and the card
336
+ * labels the button as consuming a small amount.
337
+ */
338
+ declare const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
339
+ /**
340
+ * Per-account status (check-in / credits / subscription) for the account table.
340
341
  *
341
- * HOW the generation is decided, in order:
342
+ * Each row of the account table reads its OWN state through this route — a CN
343
+ * row carries its check-in state and balance, an ai row its subscription.
344
+ * POST with the accountId in the body: one upstream read per account, paid
345
+ * only when the table is actually shown.
346
+ */
347
+ declare const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
348
+ /**
349
+ * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
350
+ * route in this plugin that changes upstream account state. The accountId in
351
+ * the body names WHICH account to claim for, so the button can sit in each
352
+ * account row.
353
+ */
354
+ declare const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
355
+ /**
356
+ * Outcome vocabulary of one account probe, shared by the Host and the card.
342
357
  *
343
- * 1. **The manifest version** of the copy this plugin resolved. Numeric semver
344
- * comparison (`minor >= 86`), so `0.86.0` and the already-shipped `1.x` line
345
- * both land on modern, and `0.9.0` correctly sorts below `0.86.0`.
346
- * 2. **The package's own layout**, when no version could be read: the transcript
347
- * module sits at `dist/utils/transcript.js` (the exports map's
348
- * `"./utils/*" → "./dist/utils/*.js"` convention is identical in every
349
- * published generation, 0.82 through 1.x).
358
+ * Mirrors `TraePoolOutcome` in `./account-pool.ts`, and is DECLARED again here
359
+ * rather than imported for the same reason workbuddy repeats it: this module is
360
+ * the node-free bridge for the browser build target and must not pull in the
361
+ * Host-side pool module (which imports the credential type). The two lists must
362
+ * stay in step — the card renders one label per value, and a value Host can send
363
+ * that the card has no label for renders as the raw token.
350
364
  *
351
- * Both checks are deliberately resolver-independent. `import.meta.resolve()` is
352
- * NOT used to answer this question: bundler-style resolvers (vite, and equally
353
- * `vitest` under it) expand the `"./utils/*"` wildcard to a path and return it
354
- * WITHOUT checking that the file exists, so a probe built on it answers "modern"
355
- * for 0.85.1 — the exact false positive that would leave issue #27 unfixed while
356
- * claiming to be capability-based. Reading the package directory cannot be
357
- * fooled that way.
365
+ * `plan-gated` is Trae-only (upstream `1005`: this ACCOUNT's plan does not cover
366
+ * the model) and has no workbuddy counterpart.
367
+ */
368
+ type TraeWebPoolOutcome = 'ok' | 'rate-limited' |
369
+ /**
370
+ * The MODEL is loaded, not the account.
358
371
  *
359
- * The version is therefore the primary signal, and it also keeps the gate
360
- * forward-compatible for free: a future 2.x needs no change here, whereas a
361
- * file-name check alone would have to know about it.
372
+ * Kept apart from `rate-limited` because the two need OPPOSITE advice:
373
+ * `rate-limited` is this account's volume wall (wait, or bill another account),
374
+ * while `model-busy` follows the model everywhere — every account hits the same
375
+ * wall, so switching only spends a request per account to learn what the first
376
+ * already said.
377
+ */
378
+ 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'unavailable' | 'not-found' | 'failed';
379
+ /** Why one account is currently out of the pool. */
380
+ type TraeWebPoolExclusion = 'unusable' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'not-found' | 'unavailable' | 'failed';
381
+ /**
382
+ * One account's row in the pool, as the card renders it.
362
383
  *
363
- * @module dsh-connect-trae/pi-ai-runtime
384
+ * Ties together the three facts the card shows per account — credits, the last
385
+ * test, and whether it can serve right now — so the ranking the card displays
386
+ * comes from ONE Host answer rather than being re-derived in the browser. The
387
+ * browser re-deriving it is how a card ends up calling an excluded account "in
388
+ * use", which is the inconsistency this document exists to prevent.
364
389
  */
365
- /** The context generation a resolved pi-ai api module speaks. */
366
- type PiAiGeneration = 'legacy' | 'modern';
367
- /** What {@link piAiRuntimeInfo} reports about the resolved pi-ai copy. */
368
- interface PiAiRuntimeInfo {
369
- /** `modern` (0.86+: consumes normalized transcripts) or `legacy` (≤0.85 shape). */
370
- generation: PiAiGeneration;
371
- /** Version of the resolved copy, when the manifest could be read. */
372
- version: string | undefined;
373
- /** Package directory of the resolved copy, when it could be located. */
374
- resolvedFrom: string | undefined;
390
+ interface TraeWebPoolAccount {
391
+ accountId: string;
392
+ accountName: string;
393
+ /** Total remaining credit, absent when this account's balance read failed. */
394
+ credits?: number;
375
395
  /**
376
- * Whether the copy could be inspected at all. `false` means pi-ai does not
377
- * resolve from this plugin — the normal state of a market install probed
378
- * OUTSIDE the DSH host, where the host supplies the module at plugin runtime.
379
- * Inside DSH the copy is always inspectable: a nested one shadows host
380
- * injection, and without a nested one the host supplies its own.
396
+ * The GENERAL bucket — the credit this plugin's chat can actually spend.
397
+ *
398
+ * Trae-only, because a Trae balance is TWO buckets and SOLO chat only drains
399
+ * General (measured on the live `available_endpoint` split). A row whose total
400
+ * looks rich but whose General reads zero cannot serve a request, which is the
401
+ * same trap workbuddy hit with its expiry rule: a ranking keyed on a figure that
402
+ * the billing path cannot reach is not a ranking. The card shows both.
381
403
  */
382
- inspected: boolean;
404
+ generalAvailable?: number;
405
+ /** The WORK bucket, kept for display: this plugin's chat never spends it. */
406
+ workAvailable?: number;
407
+ /** Credit lapsing inside 3 days. Zero means "measured, nothing at risk". */
408
+ expiringSoon?: number;
409
+ /** When the nearest pack lapses, epoch ms. */
410
+ nearestExpiryMs?: number;
411
+ /**
412
+ * This account's stored access token has lapsed, so its balance cannot be read
413
+ * until it is refreshed or the user signs in again.
414
+ *
415
+ * A reason for the dash, not a second dash: without it "0 credit" and "cannot
416
+ * read the balance" look identical on the card, and only one of them is the
417
+ * user's problem to fix.
418
+ */
419
+ tokenExpired?: boolean;
420
+ /**
421
+ * The last test of this account, absent when never tested.
422
+ *
423
+ * `modelId` lives INSIDE the measurement rather than being assumed from the
424
+ * row's current setting: the setting can change after the test, and a result
425
+ * must keep naming what it actually measured.
426
+ */
427
+ probe?: {
428
+ outcome: TraeWebPoolOutcome;
429
+ atMs: number;
430
+ /**
431
+ * What wrote this measurement, so the card can explain a change the user
432
+ * watched happen: `test-batch` is a deliberate test, `live-request` is a real
433
+ * request that failed and overwrote it. ABSENT for records written before the
434
+ * field existed — the card must then say "unknown", not guess.
435
+ */
436
+ source?: 'test-batch' | 'live-request';
437
+ /** WHICH model this measurement is about; absent = unknown scope. */
438
+ modelId?: string;
439
+ /** Only when the upstream named a time; never invented locally. */
440
+ retryAtMs?: number;
441
+ /** Upstream's own words, already redacted. Absent on success. */
442
+ message?: string;
443
+ };
444
+ /** Set when this account cannot serve right now. */
445
+ excludedBy?: TraeWebPoolExclusion;
446
+ /**
447
+ * Whether this is the account billing traffic for this panel RIGHT NOW.
448
+ *
449
+ * The pool's runtime pick when enabled, else the saved selection. It is the
450
+ * HOST's answer, not the card's guess: a card that derived "in use" from the
451
+ * saved setting would contradict the pool after a rotation.
452
+ */
453
+ current: boolean;
454
+ /**
455
+ * Whether the user checked this account into this panel's pool.
456
+ *
457
+ * Listed rows include UNCHECKED accounts so the card can offer them to check:
458
+ * membership is an explicit opt-in and a user can only opt in to something they
459
+ * can see. Only checked rows can bill or be batched.
460
+ */
461
+ member: boolean;
462
+ /**
463
+ * Whether this machine still holds a usable sign-in for the account.
464
+ *
465
+ * `false` marks a GHOST: checked or listed, but no local credential resolves, so
466
+ * a batch can neither test it nor check it in. It is kept as a row (rather than
467
+ * filtered out) because the only way to stop carrying a broken entry is to see
468
+ * it and remove it — a filtered row would leave the saved list permanently dirty
469
+ * with nothing on screen to explain why the pool runs on fewer accounts than the
470
+ * checkbox count says.
471
+ */
472
+ reachable?: boolean;
473
+ /**
474
+ * Today's check-in state for this account.
475
+ *
476
+ * ABSENT means it was not read — the card must then say so rather than render a
477
+ * definite "not checked in", which would contradict a claim the user just
478
+ * watched succeed.
479
+ */
480
+ checkedInToday?: boolean;
481
+ /**
482
+ * Whether today's check-in is spent on this MACHINE for this account.
483
+ *
484
+ * Trae-only, because Trae keys the daily claim to `x-device-id` (code `9095`).
485
+ * `checkedInToday === false` together with this true is the "switched account"
486
+ * case the check-in card already documents: this device has no claim left.
487
+ */
488
+ deviceCheckedIn?: boolean;
383
489
  }
384
- /** Resolve (once) which pi-ai copy this plugin runs against. */
385
- declare function piAiRuntimeInfo(): PiAiRuntimeInfo;
386
490
  /**
387
- * The generation decision table, exported so every branch is pinned by tests
388
- * rather than by a comment here — including the two this development tree can
389
- * never reach (no local copy, and a copy with no readable version), which are
390
- * exactly the states a market install founds its gate on.
491
+ * The pool's state for one panel.
391
492
  *
392
- * @param version - manifest version of the resolved copy, if readable.
393
- * @param transcriptPresent - whether that copy actually ships
394
- * `dist/utils/transcript.js`; `undefined` when nothing was inspected.
493
+ * `targetModelId`/`targetModelSource` exist so the card can state WHICH model a
494
+ * test would use and WHERE that choice came from. `none` is a real, common
495
+ * answer for Trae — the CN static fallback catalog carries no `creditMultiplier`
496
+ * at all until its first live refresh, and "missing multiplier ≠ free" means the
497
+ * card must render "no free model" rather than silently substituting a paid one.
395
498
  */
396
- declare function decidePiAiGeneration(version: string | undefined, transcriptPresent: boolean | undefined): PiAiGeneration;
499
+ interface TraeWebPool {
500
+ enabled: boolean;
501
+ /**
502
+ * Whether this panel offers the daily check-in at all.
503
+ *
504
+ * Only `cn` does. The ai gateways answer 404 for the whole `/trae/api/v2/ug/*`
505
+ * family (probed 2026-09-24) and the enterprise panel has no personal reward,
506
+ * so the card hides the action entirely rather than rendering a button whose
507
+ * only outcome is a refusal.
508
+ */
509
+ checkinSupported: boolean;
510
+ /** The model a test would use right now; absent when none can be resolved. */
511
+ targetModelId?: string;
512
+ /**
513
+ * Where the target came from. `'stale'` means the SAVED id is no longer in the
514
+ * catalog — a distinct state from `'none'` (no free model exists), because the
515
+ * two need opposite advice (offer a way back to automatic / say there is no
516
+ * free model at all).
517
+ */
518
+ targetModelSource: 'preferred' | 'free' | 'none' | 'stale';
519
+ /**
520
+ * How much text a pool test sends (0 = the minimal probe). Always a number so
521
+ * the card's select has a value to show; the saved preference is the default.
522
+ */
523
+ probeInputTokens: number;
524
+ /** The saved target id that this panel's catalog no longer offers. */
525
+ staleTargetModelId?: string;
526
+ /** The account ids the user has checked into this panel's pool. */
527
+ memberAccountIds?: readonly string[];
528
+ /**
529
+ * The subset of {@link TraeWebPool.memberAccountIds} that resolves to a real
530
+ * local credential — i.e. what a batch actually runs on.
531
+ *
532
+ * Sent so the card never re-derives it. Counting the raw saved list is what
533
+ * lets a UI claim "1 of 1 selected" and enable both buttons while the Host ran
534
+ * on ZERO accounts and reported success.
535
+ */
536
+ effectiveMemberAccountIds?: readonly string[];
537
+ /**
538
+ * Last measured display name per saved member id, including members that have
539
+ * since stopped resolving.
540
+ *
541
+ * A member whose sign-in disappeared would otherwise be listed as a bare id
542
+ * and an unexplained count, and the user could not tell WHICH one they unchecked.
543
+ */
544
+ memberNames?: Record<string, string>;
545
+ /** Rows in the Host's ranking order: candidates first, excluded rows last. */
546
+ accounts: readonly TraeWebPoolAccount[];
547
+ /**
548
+ * Why the pool could not pick anybody, when it could not.
549
+ *
550
+ * Set when every member is excluded, so the card can explain that traffic is
551
+ * going to the saved selection rather than leaving the user to assume the pool
552
+ * is serving the top row.
553
+ */
554
+ noCandidate?: TraeWebPoolExclusion | 'empty';
555
+ }
397
556
  /**
398
- * The generation a version string belongs to, or `undefined` when the string
399
- * cannot be read as a version at all.
557
+ * The pool's saved preferences for one panel, as stored in `regions[panel].pool`.
400
558
  *
401
- * Only the minor of the `0.x` line matters for the boundary, and `major >= 1`
402
- * is modern by definition (1.0.x is already in use by `dsh-llm-pi-ai@0.2.1-alpha.2`
403
- * and consumes transcripts natively).
559
+ * Declared in the node-free bridge because BOTH halves touch it: the Host reads
560
+ * it to decide membership and ranking, the card drafts and saves it. Keeping one
561
+ * declaration is what stops the card from writing a field the Host ignores (or
562
+ * vice versa) — the failure workbuddy hit when its client and Host each had a
563
+ * private copy of this shape.
564
+ *
565
+ * Every field is optional because this is a user-edited draft: a partially filled
566
+ * slot must still load, and a missing `enabled` means OFF rather than ON.
404
567
  */
405
- declare function generationOf(version: string | undefined): PiAiGeneration | undefined;
406
- //#endregion
407
- //#region src/upstream.d.ts
408
- type TraeUpstreamErrorKind = 'authentication' | 'hard_credit' | 'soft_rate' | 'not_found' | 'server' | 'client' | 'unconfigured';
409
- type TraeChatResult = {
410
- ok: true;
411
- response: Response;
412
- } | {
413
- ok: false;
414
- status: number;
415
- kind: TraeUpstreamErrorKind;
416
- message: string;
417
- };
418
- interface TraeUpstreamClient {
419
- chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
568
+ interface TraePoolPreferences {
569
+ /** Whether this panel bills through the pool at all. Default OFF. */
570
+ enabled?: boolean;
571
+ /**
572
+ * The model a test batch probes. Absent or `''` means "automatic": pick the
573
+ * free model (see `pickFreeModel` in `./account-pool.ts`). An id this panel no
574
+ * longer offers is PRESERVED so the user's choice survives a catalog blip, and
575
+ * reported as `stale` rather than silently replaced.
576
+ */
577
+ targetModelId?: string;
578
+ /**
579
+ * The accounts the user checked into this panel's pool, in the order they were
580
+ * checked.
581
+ *
582
+ * Explicit opt-in with NO default: an account never joins the pool because it
583
+ * exists. Order is only a display hint — the ranking is the Host's.
584
+ *
585
+ * A MUTABLE array, not `readonly`: this interface is also the shape the
586
+ * settings schema declares (`z.array(z.string())`), and the schema rebuilds the
587
+ * config document on every save — a readonly type there is a compile-time
588
+ * mismatch that would take the whole Config schema down. Readers still treat it
589
+ * as immutable; only the owner of a draft copies it.
590
+ */
591
+ memberAccountIds?: string[];
592
+ /**
593
+ * How much text a pool TEST sends, in tokens. `0` (the default) keeps the
594
+ * minimal one-word probe.
595
+ *
596
+ * Exists because a verdict earned by a one-word request says nothing about the
597
+ * long turn the user is about to send: an upstream that refuses large requests
598
+ * answers the small one happily. Padding the probe therefore buys a truthful
599
+ * "works at my size" answer at the cost of spending real credit per test.
600
+ */
601
+ probeInputTokens?: number;
420
602
  }
603
+ /** Query parameter naming the region a card request addresses. */
604
+ declare const TRAE_REGION_PARAM = "region";
605
+ /** Every region, in card tab order. */
606
+ declare const TRAE_REGIONS: readonly TraeRegion[];
421
607
  /**
422
- * Safe first-stage upstream. Real Trae traffic stays disabled until one
423
- * edition-specific endpoint and wire codec are verified with fixtures.
608
+ * Address one region's status route. The two regions are separate provider
609
+ * stacks; every card request carries the region whose tab the user is on.
424
610
  */
425
- declare class UnconfiguredTraeUpstreamClient implements TraeUpstreamClient {
426
- chatStream(_bodyJson: string, _signal?: AbortSignal): Promise<TraeChatResult>;
427
- }
428
- //#endregion
429
- //#region src/shim.d.ts
430
- interface ShimLogger {
431
- warn(...args: unknown[]): void;
432
- error(...args: unknown[]): void;
433
- }
434
- interface TraeShim {
435
- ready: Promise<void>;
436
- baseUrl(): string;
437
- token(): string;
438
- close(): Promise<void>;
611
+ declare function withTraeRegion(path: string, panel: TraePanel): string;
612
+ /**
613
+ * Read the region parameter off a status-route URL. Absent means the domestic
614
+ * tab (`cn`); a present-but-unknown value returns undefined so the route can
615
+ * answer 400 instead of guessing.
616
+ */
617
+ declare function regionOfTraeStatusUrl(url: string): TraePanel | undefined;
618
+ /** One credit pack and its remaining credit. */
619
+ interface TraeWebCreditAccount {
620
+ displayDesc: string;
621
+ remain: number;
622
+ size: number;
623
+ /**
624
+ * When this pack lapses, in epoch milliseconds, taken from the upstream's own
625
+ * `end_time`. Absent when the upstream reported no usable time (0 / missing),
626
+ * so a consumer can tell "no date" apart from "1970".
627
+ */
628
+ expiresAtMs?: number;
629
+ /**
630
+ * Raw upstream `available_endpoint`: which bucket this pack lives in.
631
+ *
632
+ * Measured on the live response — `0` is the GENERAL bucket that SOLO chat
633
+ * actually spends, `1` is the WORK bucket it cannot reach. The projection
634
+ * dropped this field until the account pool needed it: the pool ranks accounts
635
+ * by what they can SERVE, and credit lapsing in the WORK bucket is not a
636
+ * reason to bill that account first, because nothing here can ever spend it.
637
+ * Absent, not guessed, when the upstream omits it.
638
+ */
639
+ endpoint?: number;
439
640
  }
440
- interface TraeShimOptions {
441
- catalog: TraeCatalog;
442
- client: TraeUpstreamClient;
443
- logger?: ShimLogger;
641
+ /** Credit lapsing inside one horizon, as the card reports it. */
642
+ interface TraeWebExpiringCredits {
643
+ /** Sum of the contributing packs' remaining credit. */
644
+ total: number;
645
+ /** How many packs contribute. */
646
+ packs: number;
647
+ /** Soonest expiry among them, epoch milliseconds. */
648
+ earliestExpiresAtMs?: number;
444
649
  }
445
- declare function createTraeShim(options: TraeShimOptions): TraeShim;
446
- //#endregion
447
- //#region src/adapter.d.ts
448
- /** Provider route this bundle owns for the domestic (CN) gateway. */
449
- declare const TRAE_PROVIDER = "trae";
450
650
  /**
451
- * Provider route this bundle owns for the international (trae.ai) gateway.
452
- * Named `-global` to match the plugin family's convention (`workbuddy-global`);
453
- * the internal region bucket stays `ai`, which is the gateway/protocol name.
454
- */
455
- declare const TRAE_AI_PROVIDER = "trae-global";
456
- /** The provider id each region registers as. */
457
- declare const TRAE_PROVIDERS: Readonly<Record<TraeRegion, string>>;
458
- /** Human-readable provider names, shown in the DSH model picker. */
651
+ * Credit at risk, bucketed by how soon it lapses.
652
+ *
653
+ * Only present when at least one pack carried a usable `expiresAtMs` — a card
654
+ * must never read "we were not told when anything expires" as "nothing is
655
+ * about to lapse".
656
+ */
657
+ interface TraeWebCreditExpiry {
658
+ within24h: TraeWebExpiringCredits;
659
+ within3d: TraeWebExpiringCredits;
660
+ within7d: TraeWebExpiringCredits;
661
+ /** Packs already past their end time but still carrying credit. */
662
+ expired: TraeWebExpiringCredits;
663
+ }
664
+ /** Aggregated usage/credit answer rendered by the plugin card. */
665
+ interface TraeWebCredits {
666
+ total: number;
667
+ consumed: number;
668
+ available: number;
669
+ workAvailable: number;
670
+ generalAvailable: number;
671
+ accounts: readonly TraeWebCreditAccount[];
672
+ /**
673
+ * Credit lapsing soon, for the card's warning banner.
674
+ *
675
+ * Derived from the per-pack {@link TraeWebCreditAccount.expiresAtMs} the
676
+ * `/usage` route already carries, so it costs no extra upstream read.
677
+ */
678
+ expiry?: TraeWebCreditExpiry;
679
+ }
680
+ /** Editable Trae model row rendered by the plugin-owned settings card. */
681
+ interface TraeWebModel {
682
+ id: string;
683
+ name: string;
684
+ contextWindow?: number;
685
+ maxTokens?: number;
686
+ input?: ('text' | 'image')[];
687
+ creditMultiplier?: number;
688
+ reasoningSupported?: boolean;
689
+ reasoning?: {
690
+ supported: string[];
691
+ defaultEffort?: string;
692
+ };
693
+ maxContextWindow?: number;
694
+ /**
695
+ * Trae's own image-capability flag, as the runtime catalog carries it. The
696
+ * card renders these rows with their image checkbox already ticked so the
697
+ * control matches what DSH is actually told (issue #16); absent means the
698
+ * directory did not declare it, which is not a capability claim either way.
699
+ */
700
+ multimodal?: boolean;
701
+ /**
702
+ * The availability verdict from the one-click test, when this card session
703
+ * has run one. Absent = not tested. Deliberately NOT persisted with the
704
+ * settings: it describes one moment on one account, and a stale verdict shown
705
+ * as current is worse than a blank field.
706
+ */
707
+ probe?: TraeModelProbeVerdict;
708
+ /** Upstream's own words for the verdict, when it gave any. */
709
+ probeDetail?: string;
710
+ }
711
+ /**
712
+ * One model's availability verdict from the one-click test.
713
+ *
714
+ * `unknown` is a first-class outcome, not a failure: a timeout, a proxy drop or
715
+ * a throttle says nothing about the model, and reporting it as unusable would
716
+ * make the user delete a model that works. Only an upstream refusal is negative.
717
+ */
718
+ type TraeModelProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
719
+ /** What the probe route answers with. */
720
+ interface TraeModelsTestResponse {
721
+ /** One entry per model tested, in the order they were sent. */
722
+ results: {
723
+ id: string;
724
+ verdict: TraeModelProbeVerdict;
725
+ detail: string;
726
+ }[];
727
+ /** Models skipped because the region has no live catalog row for them. */
728
+ skipped?: string[];
729
+ }
730
+ /**
731
+ * Daily check-in state rendered below the credit stats.
732
+ *
733
+ * The two booleans answer DIFFERENT questions and must never be collapsed into
734
+ * one "done" flag — doing that was a shipped bug (see CHANGELOG 2.3.1):
735
+ *
736
+ * - `checkedIn` is ACCOUNT-level: today's reward pack exists for this account,
737
+ * so there is nothing left to claim and the balance already includes it.
738
+ * - `didCheckedIn` is DEVICE-level: this MACHINE already spent its one
739
+ * check-in for the day, whichever account spent it. The upstream keys it on
740
+ * the `x-device-id` in the request and answers it per device, not per
741
+ * account (measured 2026-09-26: the same account and Beijing day answered
742
+ * `false` with the header omitted, with a synthetic id, and with another
743
+ * install's id, and `true` for the device that actually claimed).
744
+ *
745
+ * So `didCheckedIn && !checkedIn` is the "switched account" case: the device's
746
+ * daily check-in is used up, but THIS account was never rewarded. Claiming
747
+ * again is refused upstream (business code 9095, "该设备今日已参与签到"), so
748
+ * the card disables the button — but it must say that, not "claimed today".
749
+ */
750
+ interface TraeWebCheckin {
751
+ checkedIn: boolean;
752
+ didCheckedIn: boolean;
753
+ credits: number;
754
+ enabled: boolean;
755
+ extraCredits?: number;
756
+ }
757
+ /** The claim route's answer, so the card can report a business refusal verbatim. */
758
+ interface TraeWebCheckinClaim {
759
+ /** False when the upstream answered a non-zero business code (e.g. 9004). */
760
+ claimed: boolean;
761
+ /**
762
+ * True only when THIS ACCOUNT's reward for today already exists, so nothing
763
+ * was sent upstream. Distinct from {@link TraeWebCheckinClaim.deviceCheckedIn}:
764
+ * a device that already claimed for another account leaves this false.
765
+ */
766
+ alreadyCheckedIn: boolean;
767
+ /**
768
+ * True when today's check-in was already spent on this DEVICE (by this or
769
+ * another account), so no claim was sent or the upstream refused it with
770
+ * 9095. Not an error: it is the truthful answer to "can this account claim
771
+ * here today?" — no, and the reason is the machine, not the account.
772
+ */
773
+ deviceCheckedIn?: boolean;
774
+ code?: number;
775
+ message?: string;
776
+ /** The refreshed check-in state, present whenever the claim path ran. */
777
+ checkin?: TraeWebCheckin;
778
+ }
779
+ /** One reward activity rule (name + work/general credits when present). */
780
+ interface TraeWebActivity {
781
+ activityId: string;
782
+ enabled: boolean;
783
+ workCredits?: number;
784
+ generalCredits?: number;
785
+ }
786
+ interface TraeWebAccount {
787
+ id: string;
788
+ accountName: string;
789
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
790
+ /**
791
+ * Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
792
+ * pool. The enterprise tab lists only these, because the tenant gateway
793
+ * serves a directory to any CN token but refuses a personal account's chat
794
+ * with `4019` — so nothing else reveals the difference until a turn fails.
795
+ */
796
+ enterprise: boolean;
797
+ /** Routing bucket of this account, derived from its credential. */
798
+ region: TraeRegion;
799
+ source: 'desktop' | 'dsh' | 'cli';
800
+ tokenExpiresAtMs: number;
801
+ /**
802
+ * When this account's REFRESH token expires, when the credential carries one.
803
+ *
804
+ * The access token above is renewed automatically — the plugin refreshes it
805
+ * five minutes before it lapses — so by itself it answers "when does this
806
+ * need attention", which is "never, while the refresh token lives". The
807
+ * refresh token is the real horizon: once IT expires the automatic renewal
808
+ * stops working and the user must sign in to Trae again. Before this field the
809
+ * card could only say "auto-renewal", which stays true right up to the moment
810
+ * it silently stops being true.
811
+ */
812
+ refreshExpiresAtMs?: number;
813
+ selected: boolean;
814
+ }
815
+ /**
816
+ * The account the Trae desktop app is signed in to right now, when it differs
817
+ * from the one this plugin is bound to (issue #29).
818
+ *
819
+ * Reported on the usage document so the card can offer one-click follow without
820
+ * an extra round trip; also written back on a POST to the accounts refresh
821
+ * route when the client confirms it saw the hint. Absent whenever the two
822
+ * agree or the app is not readable — a healthy document carries no extra
823
+ * fields and the plugin never bills from this value directly: the FOLLOW is a
824
+ * settings write of `accounts.<region>`, made by the card or by the host's
825
+ * follow switch, never a silent rebind here.
826
+ */
827
+ interface TraeWebAppLogin {
828
+ id: string;
829
+ accountName: string;
830
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
831
+ region: TraeRegion;
832
+ }
833
+ /**
834
+ * The per-account state shown inside an account row of the card's table.
835
+ *
836
+ * Every locally detected account in a region is rendered as a row; the
837
+ * interesting per-account facts differ by region, which is why this is a union
838
+ * of the three surfaces the card previously showed only for the BOUND account:
839
+ *
840
+ * - CN rows carry their own check-in state (Trae keys the daily reward per
841
+ * ACCOUNT, and the one-per-device tally per DEVICE, see {@link TraeWebCheckin})
842
+ * and their Work/General balances.
843
+ * - ai rows carry their own subscription / trial status.
844
+ *
845
+ * Each field is optional: a read that fails keeps the row visible (perhaps
846
+ * without a figure) rather than dropping the account and its switch.
847
+ */
848
+ interface TraeWebAccountState {
849
+ /** The account this state belongs to. */
850
+ id: string;
851
+ /** CN: today's check-in state of this account (absent for ai rows). */
852
+ checkin?: TraeWebCheckin;
853
+ /** CN: this account's Work/General balances (absent for ai rows). */
854
+ credits?: TraeWebCredits;
855
+ /** ai: this account's subscription / trial status (absent for CN rows). */
856
+ payStatus?: TraeWebPayStatus;
857
+ }
858
+ interface TraeWebCreditAlternative {
859
+ id: string;
860
+ accountName: string;
861
+ edition: TraeWebAccount['edition'];
862
+ workAvailable: number;
863
+ generalAvailable: number;
864
+ }
865
+ /** One probed candidate path and why it did not yield an account. */
866
+ interface TraeWebSearchPath {
867
+ path: string;
868
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
869
+ source: 'desktop' | 'cli';
870
+ reason: 'missing' | 'unreadable' | 'invalid';
871
+ message?: string;
872
+ }
873
+ /**
874
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
875
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
876
+ */
877
+ declare function unwrapVolatile<T>(value: T): T;
878
+ /**
879
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
880
+ * value it resolves to.
881
+ *
882
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
883
+ * reference is still `typeof === 'object'` — so it passes a naive object check
884
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
885
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
886
+ * spreads a resolved field to preserve its siblings — the card's "write one
887
+ * region, keep the other" write, or the account map's spread — would otherwise
888
+ * DROP every sibling and leak a function into the document.
889
+ *
890
+ * Both halves therefore unwrap before touching a resolved field. This lives in
891
+ * the node-free host↔client bridge because the browser half needs it too.
892
+ *
893
+ * Non-reference values are recursed into so a nested volatile field is caught
894
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
895
+ * value is never touched.
896
+ */
897
+ declare function unwrapVolatileDeep<T>(value: T): T;
898
+ /**
899
+ * Read the per-region account selections out of a settings value.
900
+ *
901
+ * SHARED because `accounts` now has TWO writers: the card, and the composer
902
+ * panel's account table (2.14.0). Two writers of one field must use one merge
903
+ * rule, or they drift — and the rule is not obvious.
904
+ *
905
+ * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
906
+ * live reference, which passes the `typeof === 'object'` check and would be
907
+ * returned as if it were the map. Every lookup on it is then `undefined`, and
908
+ * the merge (`{ ...configured, [region]: id }`) would spread a reference into
909
+ * `{get: <function>}` — dropping the other region's selection and leaking a
910
+ * function into the settings document.
911
+ */
912
+ declare function configuredAccountsOf(configured: unknown): Record<string, string>;
913
+ /**
914
+ * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
915
+ * #29). Absent reads as OFF, so an existing configuration keeps the strict
916
+ * bind it shipped with and the switch is an opt-in — the same opt-out
917
+ * semantics as {@link regionEnabledOf}, for the same reason: a config written
918
+ * before this field existed cannot ask for behavior it never declared.
919
+ *
920
+ * Reads the whole settings section (or the bare field); deep-unwraps first
921
+ * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
922
+ * reference, whose `typeof === 'object'` would otherwise pass a naive check.
923
+ */
924
+ declare function followAppLoginEnabled(configured: unknown): boolean;
925
+ /**
926
+ * Build the next `regions` settings value for the card's save. The write
927
+ * targets ONLY the signed-in account's region slot; every other region's slot
928
+ * is carried over untouched, so switching accounts never clobbers the other
929
+ * region's picks. Tolerates any stored shape (absent, non-object) by starting
930
+ * from an empty document.
931
+ */
932
+ declare function nextRegionSlots<Slot extends object>(regions: unknown, panel: TraePanel, slot: Slot): Record<string, unknown>;
933
+ /**
934
+ * Whether one region's provider is switched on. Opt-out semantics: only an
935
+ * explicit `false` disables it, so a config written before this switch existed
936
+ * (and the pre-region-split flat fields, which never carry `enabled`) keep both
937
+ * providers running exactly as before. The Host reads the same rule through
938
+ * `regionStateOf`, so card and Host can never disagree about a region's state.
939
+ *
940
+ * `value` may be the whole settings section or the `regions` map (see
941
+ * {@link regionsMapOf}).
942
+ */
943
+ declare function regionEnabledOf(value: unknown, panel: TraePanel): boolean;
944
+ /**
945
+ * Build the next `regions` settings value for a provider on/off toggle. ONLY
946
+ * the target region's `enabled` flag changes: every other field of that slot
947
+ * (its directory, selection, image opt-ins, context budgets) and every other
948
+ * region's slot are carried over verbatim, so switching a provider off never
949
+ * discards the user's model picks and switching it back on restores them.
950
+ *
951
+ * This is deliberately separate from {@link nextRegionSlots}: that helper
952
+ * writes a whole slot from a signed-in tab's draft, while this one must work
953
+ * for a region that is signed OUT — which is precisely the region a user wants
954
+ * to switch off (no international install, no international account).
955
+ *
956
+ * `value` may be the whole settings section or the `regions` map; the RETURN
957
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
958
+ * 'regions', ...)` needs.
959
+ */
960
+ declare function nextRegionEnabled(value: unknown, panel: TraePanel, enabled: boolean): Record<string, unknown>;
961
+ /**
962
+ * The two seats this region's credit readout can occupy.
963
+ *
964
+ * `composer` is the composer tool row (`conversation.input.left`); `stats` is
965
+ * the ambient dock below the composer card (`conversation.composer.dock`),
966
+ * beside the shell's own round/step/token pills.
967
+ *
968
+ * WHY THE SEAT IS A CHOICE AT ALL (learned from dsh-connect-workbuddy issue
969
+ * #62): the composer tool row is a LIST seat, so it is shared not only with the
970
+ * shell's own controls but with any other plugin that registers there — and a
971
+ * list seat cannot promise that every occupant is visible. The report behind
972
+ * that issue is exactly that: with a third-party plugin installed the readout
973
+ * stopped appearing. A fixed seat therefore lets one plugin's presence decide
974
+ * another's, so the seat belongs to the user.
975
+ *
976
+ * Named as a union rather than a boolean because a third seat is plausible and
977
+ * a `showAtTop: boolean` would have to be MIGRATED rather than extended.
978
+ */
979
+ type TraeCreditPosition = 'composer' | 'stats';
980
+ /**
981
+ * Which seat one region's readout uses, defaulting to the composer row.
982
+ *
983
+ * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
984
+ * the two cases that matter: a profile written BEFORE this setting existed (the
985
+ * composer row was the only seat, so an upgrade moves nothing on screen), and a
986
+ * hand-edited value the schema does not know. Falling back rather than
987
+ * rendering nowhere is deliberate — an unrecognized value must not make the
988
+ * readout disappear, which is the very defect this setting exists to fix.
989
+ *
990
+ * `value` may be the whole settings section or the `regions` map (see
991
+ * {@link regionsMapOf}).
992
+ */
993
+ declare function regionCreditPositionOf(value: unknown, panel: TraePanel): TraeCreditPosition;
994
+ /**
995
+ * Build the next `regions` settings value for one region's credit seat.
996
+ *
997
+ * Same merge rule as every sibling region write: only the target region's
998
+ * `creditPosition` changes, so a seat change cannot drop that slot's model
999
+ * directory, selection, budgets or enabled flag, nor touch the other region.
1000
+ *
1001
+ * `value` may be the whole settings section or the `regions` map; the RETURN
1002
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1003
+ * 'regions', ...)` needs.
1004
+ */
1005
+ declare function nextRegionCreditPosition(value: unknown, panel: TraePanel, position: TraeCreditPosition): Record<string, unknown>;
1006
+ /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
1007
+ interface TraeWebPayStatus {
1008
+ isDollarUsageBilling: boolean;
1009
+ hasPackage: boolean;
1010
+ isPayFreshman: boolean;
1011
+ inTrial: boolean;
1012
+ trialEndTimeMs: number;
1013
+ enableSoloLite: boolean;
1014
+ enableSoloBuilder: boolean;
1015
+ enableSoloCoder: boolean;
1016
+ enableSoloWeb: boolean;
1017
+ /** Trae's authoritative tier number (`user_pay_identity`); `0` means Free. */
1018
+ payIdentity?: number;
1019
+ /** Trae's authoritative tier label (`user_pay_identity_str`), e.g. `Free`. */
1020
+ payIdentityStr?: string;
1021
+ fission?: {
1022
+ startTimeMs: number;
1023
+ expireTimeMs: number;
1024
+ maxUsage: number;
1025
+ };
1026
+ }
1027
+ /** The JSON document the plugin card renders. */
1028
+ type TraeWebUsage = {
1029
+ status: 'signed-out';
1030
+ accounts: readonly TraeWebAccount[];
1031
+ message?: string;
1032
+ searched?: readonly TraeWebSearchPath[];
1033
+ enabled?: boolean;
1034
+ } | {
1035
+ status: 'signed-in';
1036
+ accountId: string;
1037
+ accountName: string;
1038
+ tokenExpiresAtMs: number;
1039
+ /**
1040
+ * When the BOUND account's refresh token expires (absent when the credential
1041
+ * carries none). See {@link TraeWebAccount.refreshExpiresAtMs} — this is the
1042
+ * horizon that actually matters, because the access token above renews
1043
+ * itself until this one lapses.
1044
+ */
1045
+ refreshExpiresAtMs?: number;
1046
+ /** Which per-region model directory and selection this account owns. */
1047
+ region: TraeRegion;
1048
+ /** Whether this region's provider is currently offered to DSH. */
1049
+ enabled?: boolean;
1050
+ accounts: readonly TraeWebAccount[];
1051
+ models: readonly TraeWebModel[];
1052
+ enabledModelIds: readonly string[];
1053
+ rawChat?: {
1054
+ state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
1055
+ checkedAtMs?: number;
1056
+ status?: number;
1057
+ };
1058
+ credits?: TraeWebCredits;
1059
+ creditsError?: string;
1060
+ /**
1061
+ * Set when this panel has no readable balance by design, rather than by
1062
+ * failure. Only `enterprise` does: a tenant's spend is not reported by any
1063
+ * endpoint the plugin can reach (every tenant quota path answers 404), so
1064
+ * the figure exists solely in the enterprise console. The card must say so
1065
+ * instead of rendering the consumer packs, which belong to the personal
1066
+ * balance and would be read as the tenant's.
1067
+ */
1068
+ creditsUnavailable?: 'enterprise';
1069
+ /**
1070
+ * Present only when the bound account is out of credit AND another account
1071
+ * on this machine has some (issue #25). The card then offers a one-click
1072
+ * switch instead of leaving a permanently empty panel. Absent in every
1073
+ * other case so a healthy account never pays for the extra read.
1074
+ */
1075
+ alternatives?: readonly TraeWebCreditAlternative[];
1076
+ /**
1077
+ * Present only when the Trae app's CURRENT sign-in differs from the
1078
+ * account this plugin is bound to (issue #29): the card then offers a
1079
+ * one-click follow instead of making the user hunt the account dropdown
1080
+ * after every app-side sign-out. Absent in every other case — a healthy
1081
+ * document costs no extra read beyond the accounts scan the document
1082
+ * already runs.
1083
+ */
1084
+ appLogin?: TraeWebAppLogin;
1085
+ /**
1086
+ * Daily check-in state (CN only). Absent on the international region,
1087
+ * whose check-in surface does not exist — probed 2026-09-24: the
1088
+ * `/trae/api/v2/ug/*` family answers 404 on every ai gateway, so the card
1089
+ * shows no claim button there rather than a button that cannot work.
1090
+ */
1091
+ checkin?: TraeWebCheckin;
1092
+ checkinError?: string;
1093
+ /** Subscription status of an international account (region ai only). */
1094
+ payStatus?: TraeWebPayStatus;
1095
+ payStatusError?: string;
1096
+ /**
1097
+ * This panel's account-pool state, when the Host build supports it.
1098
+ *
1099
+ * Absent on a Host without pool support, so the card renders no pool section
1100
+ * instead of an empty one. Assembled LAST in the status document and never
1101
+ * allowed to fail it: the pool reads per-account credit, and one dead account
1102
+ * must not blank out the panel the user is looking at.
1103
+ */
1104
+ pool?: TraeWebPool;
1105
+ } | {
1106
+ status: 'error';
1107
+ message: string;
1108
+ };
1109
+ //#endregion
1110
+ //#region src/pi-ai-runtime.d.ts
1111
+ /**
1112
+ * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
1113
+ * context shape that copy consumes (issue #27).
1114
+ *
1115
+ * WHY THIS EXISTS. The plugin declares pi-ai as a peer, so the module it
1116
+ * imports is whatever the install provides — and the two generations consume
1117
+ * DIFFERENT context shapes:
1118
+ *
1119
+ * - **modern** (0.86+): every public entry point calls `normalizeContext()`
1120
+ * once, so what reaches a provider is a normalized *transcript*:
1121
+ * `{ messages: [ { role: 'system', content, toolsAdded }, … ] }`. The system
1122
+ * prompt and the tool declarations live ONLY on that leading system message;
1123
+ * the top-level `systemPrompt` / `tools` fields are gone, and the api never
1124
+ * reads them back.
1125
+ * - **legacy** (≤0.85): expects `{ systemPrompt, tools, messages }` and has no
1126
+ * branch for a `system` message inside `messages` at all. Its token estimator
1127
+ * crashes on one — `estimateMessageTokens` iterates the string `content`
1128
+ * character by character and reads `.name` off a character — and its wire
1129
+ * converter silently drops it.
1130
+ *
1131
+ * The mismatch is reachable in BOTH directions, because DSH itself spans both
1132
+ * generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
1133
+ * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`, and 0.2.1-rc.1 —
1134
+ * the current desktop host — declares **`^1.1.0`**, which also stopped reading
1135
+ * the top-level fields entirely), while this plugin accepts the whole range
1136
+ * (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer, the other one is wrong:
1137
+ *
1138
+ * | host | resolved api | outcome |
1139
+ * |---|---|---|
1140
+ * | legacy | legacy | fine |
1141
+ * | legacy | modern | no crash, but prompt + tools silently LOST |
1142
+ * | modern | legacy | CRASH: `Cannot read properties of undefined (reading 'length')` |
1143
+ * | modern | modern | fine |
1144
+ *
1145
+ * THE BOUNDARY IS 0.86, NOT 0.87. Checked against the published tarballs:
1146
+ * `dist/utils/transcript.js` — and with it `normalizeContext`, the
1147
+ * `role === 'system'` estimator branch, and the switch away from reading
1148
+ * `systemPrompt`/`tools` — first appears in **0.86.0**. A `>= 0.87` gate
1149
+ * misclassifies 0.86.x as legacy and folds a context the modern api consumes
1150
+ * natively, which deletes the only carrier of the prompt and the tools.
1151
+ *
1152
+ * HOW the generation is decided, in order:
1153
+ *
1154
+ * 1. **The manifest version** of the copy this plugin resolved. Numeric semver
1155
+ * comparison (`minor >= 86`), so `0.86.0` and the already-shipped `1.x` line
1156
+ * both land on modern, and `0.9.0` correctly sorts below `0.86.0`.
1157
+ * 2. **The package's own layout**, when no version could be read: the transcript
1158
+ * module sits at `dist/utils/transcript.js` (the exports map's
1159
+ * `"./utils/*" → "./dist/utils/*.js"` convention is identical in every
1160
+ * published generation, 0.82 through 1.x).
1161
+ *
1162
+ * Both checks are deliberately resolver-independent. `import.meta.resolve()` is
1163
+ * NOT used to answer this question: bundler-style resolvers (vite, and equally
1164
+ * `vitest` under it) expand the `"./utils/*"` wildcard to a path and return it
1165
+ * WITHOUT checking that the file exists, so a probe built on it answers "modern"
1166
+ * for 0.85.1 — the exact false positive that would leave issue #27 unfixed while
1167
+ * claiming to be capability-based. Reading the package directory cannot be
1168
+ * fooled that way.
1169
+ *
1170
+ * The version is therefore the primary signal, and it also keeps the gate
1171
+ * forward-compatible for free: a future 2.x needs no change here, whereas a
1172
+ * file-name check alone would have to know about it.
1173
+ *
1174
+ * @module dsh-connect-trae/pi-ai-runtime
1175
+ */
1176
+ /** The context generation a resolved pi-ai api module speaks. */
1177
+ type PiAiGeneration = 'legacy' | 'modern';
1178
+ /** What {@link piAiRuntimeInfo} reports about the resolved pi-ai copy. */
1179
+ interface PiAiRuntimeInfo {
1180
+ /** `modern` (0.86+: consumes normalized transcripts) or `legacy` (≤0.85 shape). */
1181
+ generation: PiAiGeneration;
1182
+ /** Version of the resolved copy, when the manifest could be read. */
1183
+ version: string | undefined;
1184
+ /** Package directory of the resolved copy, when it could be located. */
1185
+ resolvedFrom: string | undefined;
1186
+ /**
1187
+ * Whether the copy could be inspected at all. `false` means pi-ai does not
1188
+ * resolve from this plugin — the normal state of a market install probed
1189
+ * OUTSIDE the DSH host, where the host supplies the module at plugin runtime.
1190
+ * Inside DSH the copy is always inspectable: a nested one shadows host
1191
+ * injection, and without a nested one the host supplies its own.
1192
+ */
1193
+ inspected: boolean;
1194
+ }
1195
+ /** Resolve (once) which pi-ai copy this plugin runs against. */
1196
+ declare function piAiRuntimeInfo(): PiAiRuntimeInfo;
1197
+ /**
1198
+ * The generation decision table, exported so every branch is pinned by tests
1199
+ * rather than by a comment here — including the two this development tree can
1200
+ * never reach (no local copy, and a copy with no readable version), which are
1201
+ * exactly the states a market install founds its gate on.
1202
+ *
1203
+ * @param version - manifest version of the resolved copy, if readable.
1204
+ * @param transcriptPresent - whether that copy actually ships
1205
+ * `dist/utils/transcript.js`; `undefined` when nothing was inspected.
1206
+ */
1207
+ declare function decidePiAiGeneration(version: string | undefined, transcriptPresent: boolean | undefined): PiAiGeneration;
1208
+ /**
1209
+ * The generation a version string belongs to, or `undefined` when the string
1210
+ * cannot be read as a version at all.
1211
+ *
1212
+ * Only the minor of the `0.x` line matters for the boundary, and `major >= 1`
1213
+ * is modern by definition (1.0.x is already in use by `dsh-llm-pi-ai@0.2.1-alpha.2`
1214
+ * and consumes transcripts natively).
1215
+ */
1216
+ declare function generationOf(version: string | undefined): PiAiGeneration | undefined;
1217
+ //#endregion
1218
+ //#region src/upstream.d.ts
1219
+ type TraeUpstreamErrorKind = 'authentication' | 'hard_credit' | 'soft_rate' | 'not_found' | 'server' | 'client' | 'unconfigured';
1220
+ type TraeChatResult = {
1221
+ ok: true;
1222
+ response: Response;
1223
+ } | {
1224
+ ok: false;
1225
+ status: number;
1226
+ kind: TraeUpstreamErrorKind;
1227
+ message: string;
1228
+ };
1229
+ interface TraeUpstreamClient {
1230
+ chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
1231
+ }
1232
+ /**
1233
+ * Safe first-stage upstream. Real Trae traffic stays disabled until one
1234
+ * edition-specific endpoint and wire codec are verified with fixtures.
1235
+ */
1236
+ declare class UnconfiguredTraeUpstreamClient implements TraeUpstreamClient {
1237
+ chatStream(_bodyJson: string, _signal?: AbortSignal): Promise<TraeChatResult>;
1238
+ }
1239
+ //#endregion
1240
+ //#region src/shim.d.ts
1241
+ interface ShimLogger {
1242
+ warn(...args: unknown[]): void;
1243
+ error(...args: unknown[]): void;
1244
+ }
1245
+ interface TraeShim {
1246
+ ready: Promise<void>;
1247
+ baseUrl(): string;
1248
+ token(): string;
1249
+ /** Bearer secret of one named route (defaults to the first route). */
1250
+ tokenOf(label?: string): string;
1251
+ close(): Promise<void>;
1252
+ }
1253
+ /** One bearer-authenticated chat route served by the shim. */
1254
+ interface TraeShimRoute {
1255
+ /** Stable label used to look the route's secret up (`solo` | `enterprise`). */
1256
+ label: string;
1257
+ client: TraeUpstreamClient;
1258
+ /**
1259
+ * Account-pool hooks for this route (issue #44). Absent means "one fixed
1260
+ * account, no rotation and no failover" — the behaviour before the pool
1261
+ * existed, and still the behaviour when the pool is switched off.
1262
+ */
1263
+ pool?: TraeShimPool;
1264
+ }
1265
+ /**
1266
+ * What an account pool tells the shim around one chat attempt.
1267
+ *
1268
+ * The shim owns the HTTP surface and the pool owns "who bills", so the two meet
1269
+ * here: the pool may re-point the next request, hand back a client for a
1270
+ * different account after a failure, and hear about failures that only a real
1271
+ * request — not a probe — can produce.
1272
+ *
1273
+ * Every method must resolve rather than throw: a pool failure must never turn a
1274
+ * chat request into a 500 when a plain answer was possible.
1275
+ */
1276
+ interface TraeShimPool {
1277
+ /**
1278
+ * Choose the account for this request and report which one it is. Called once,
1279
+ * before the first attempt; `undefined` means "the pool chose nobody", which
1280
+ * leaves the route's own client to bill the bound account.
1281
+ */
1282
+ prepare(): Promise<string | undefined>;
1283
+ /**
1284
+ * The client for the next account worth trying after the current one failed,
1285
+ * or `undefined` when the pool has nothing left to try.
1286
+ *
1287
+ * The returned client must be bound to that account's OWN credential:
1288
+ * failing over is not a rebind, so it must not move the account the user
1289
+ * selected (the same rule per-account probes already follow).
1290
+ */
1291
+ next(tried: readonly string[]): Promise<{
1292
+ accountId: string;
1293
+ client: TraeUpstreamClient;
1294
+ } | undefined>;
1295
+ /** Record a real request's failure against the account that hit it. */
1296
+ failed(accountId: string, kind: TraeUpstreamErrorKind, message: string): Promise<void>;
1297
+ /** One clause naming why no other account can serve, for the error text. */
1298
+ shortfall(tried: readonly string[]): Promise<string | undefined>;
1299
+ }
1300
+ interface TraeShimOptions {
1301
+ catalog: TraeCatalog;
1302
+ /**
1303
+ * Single-route form, kept for the existing call sites and tests.
1304
+ *
1305
+ * A shim serves exactly as many chat routes as the region publishes provider
1306
+ * ids, and they must not be interchangeable: the consumer and enterprise
1307
+ * pools bill different accounts, so the route is chosen by the bearer token
1308
+ * the adapter was built with, and an unknown token is rejected rather than
1309
+ * defaulting to "some" pool.
1310
+ */
1311
+ client?: TraeUpstreamClient;
1312
+ routes?: readonly TraeShimRoute[];
1313
+ logger?: ShimLogger;
1314
+ }
1315
+ declare function createTraeShim(options: TraeShimOptions): TraeShim;
1316
+ //#endregion
1317
+ //#region src/adapter.d.ts
1318
+ /** Provider route this bundle owns for the domestic (CN) gateway. */
1319
+ declare const TRAE_PROVIDER = "trae";
1320
+ /**
1321
+ * Provider route this bundle owns for the CN **enterprise (tenant)** gateway.
1322
+ *
1323
+ * A separate provider id, not a mode switch, because the two pools bill
1324
+ * differently: a tenant account (`tob_online`) is served by BOTH gateways with
1325
+ * the SAME credential, and only the enterprise one draws on the enterprise
1326
+ * pool. Exposing one id would force a per-call guess about who pays.
1327
+ *
1328
+ * It never falls back to the consumer pool: picking this provider is the user
1329
+ * saying "bill the tenant", so a refusal is reported as an error instead of
1330
+ * being silently re-run against the personal quota.
1331
+ */
1332
+ declare const TRAE_ENTERPRISE_PROVIDER = "trae-enterprise";
1333
+ /**
1334
+ * Provider route this bundle owns for the international (trae.ai) gateway.
1335
+ * Named `-global` to match the plugin family's convention (`workbuddy-global`);
1336
+ * the internal region bucket stays `ai`, which is the gateway/protocol name.
1337
+ */
1338
+ declare const TRAE_AI_PROVIDER = "trae-global";
1339
+ /** The provider ids one region publishes, in model-picker order. */
1340
+ declare const TRAE_PROVIDER_IDS: Readonly<Record<TraeRegion, readonly string[]>>;
1341
+ /** Each region's default (first) provider id. */
1342
+ declare const TRAE_PROVIDERS: Readonly<Record<TraeRegion, string>>;
1343
+ /**
1344
+ * Which chat pool a provider bills against.
1345
+ *
1346
+ * `trae` and `trae-global` keep the behaviour they always had (the consumer
1347
+ * SOLO pool); `trae-enterprise` is the only id that reaches the tenant gateway.
1348
+ */
1349
+ type TraeProviderRoute = 'solo' | 'enterprise';
1350
+ declare const TRAE_PROVIDER_ROUTES: Readonly<Record<string, TraeProviderRoute>>;
1351
+ /** Chat pool a provider id bills against; `undefined` for an unknown id. */
1352
+ declare function routeOfTraeProvider(provider: string): TraeProviderRoute | undefined;
1353
+ /** Human-readable provider names, shown in the DSH model picker. */
459
1354
  declare const TRAE_PROVIDER_DISPLAY_NAMES: Readonly<Record<TraeRegion, string>>;
1355
+ /** Display name per provider id (the region table above only names the default). */
1356
+ declare const TRAE_PROVIDER_ID_DISPLAY_NAMES: Readonly<Record<string, string>>;
1357
+ /** Display name of a provider id. */
1358
+ declare function displayNameOfTraeProvider(provider: string): string;
460
1359
  /** Region a provider route id belongs to. */
461
1360
  declare function regionOfTraeProvider(provider: string): TraeRegion | undefined;
462
1361
  declare const TRAE_STREAM_IDLE_TIMEOUT_MS = 300000;
@@ -553,6 +1452,14 @@ interface TraeAdapterOptions {
553
1452
  provider?: string;
554
1453
  /** pi-ai provider name and profile display name; defaults to the CN name. */
555
1454
  displayName?: string;
1455
+ /**
1456
+ * Bearer secret this provider presents to the shim.
1457
+ *
1458
+ * A region can publish more than one provider (consumer + enterprise), and
1459
+ * the shim picks the billing pool from that bearer. Defaults to the shim's
1460
+ * first route, which keeps the single-provider case unchanged.
1461
+ */
1462
+ secret?: string;
556
1463
  resolveAttachments?: () => AttachmentStore | undefined;
557
1464
  /**
558
1465
  * Override the resolved pi-ai generation (see {@link withContextAdaptation});
@@ -573,6 +1480,19 @@ interface TraeCredential {
573
1480
  refreshToken?: string;
574
1481
  userId: string;
575
1482
  accountName?: string;
1483
+ /**
1484
+ * Trae tenant this account belongs to (`account.tenant_id`, e.g. `272193024`
1485
+ * for a `tob_online` enterprise), absent for a personal account.
1486
+ *
1487
+ * This is what decides whether the ENTERPRISE pool can serve the account at
1488
+ * all. The tenant gateway accepts any CN token for its DIRECTORY read — a
1489
+ * personal account gets the full 49-model roster back — so "the roster
1490
+ * loaded" proves nothing about billing rights. Its chat endpoint is what
1491
+ * refuses: a personal account answers `4019` (risk control), a tenant account
1492
+ * streams normally. Because the two are indistinguishable from the roster,
1493
+ * the account list itself has to carry the flag.
1494
+ */
1495
+ tenantId?: string;
576
1496
  host: string;
577
1497
  /**
578
1498
  * Region claim from the decrypted storage document (`userRegion.region`,
@@ -603,6 +1523,27 @@ interface TraeCredentialStoreOptions {
603
1523
  * stores run side by side without seeing each other's accounts.
604
1524
  */
605
1525
  region?: TraeRegion;
1526
+ /**
1527
+ * Only consider credentials that belong to a Trae tenant.
1528
+ *
1529
+ * The enterprise panel's store runs with this on: a personal account cannot
1530
+ * bill that pool, and because the tenant gateway still serves it a directory,
1531
+ * binding it here would fail only at the first chat (`4019`). With the flag
1532
+ * set, an explicit binding to a personal account falls through to the preferred
1533
+ * TENANT account instead of being silently honoured.
1534
+ */
1535
+ requireTenant?: boolean;
1536
+ /**
1537
+ * Withhold TENANT (enterprise) accounts from this store's account list.
1538
+ *
1539
+ * Set on the consumer panels: an enterprise login belongs to the enterprise
1540
+ * panel, and offering it here would let a tenant account be bound to the
1541
+ * personal pool without the user ever seeing which pool they picked. A
1542
+ * machine that has ONLY a tenant login still resolves it — an empty list must
1543
+ * not turn a working install into "signed out" — so the filter is applied to
1544
+ * the OFFERED list, never to the resolved credential.
1545
+ */
1546
+ hideTenantAccounts?: boolean;
606
1547
  /** Legacy single-copy path read as a migration source; injectable for tests. */
607
1548
  legacyOwnPath?: string;
608
1549
  refresh: (credential: TraeCredential) => Promise<TraeRefreshOutcome>;
@@ -612,6 +1553,16 @@ interface TraeAccountChoice {
612
1553
  id: string;
613
1554
  accountName: string;
614
1555
  edition: TraeEdition;
1556
+ /**
1557
+ * Whether this account may bill the CN ENTERPRISE pool.
1558
+ *
1559
+ * Derived from the credential's own tenant claim, because nothing else can
1560
+ * tell: the tenant gateway answers a directory read for ANY CN token, so a
1561
+ * personal account looks perfectly serviceable right up to its first chat,
1562
+ * which it answers with `4019`. The enterprise panel filters its account list
1563
+ * and its default binding on this flag.
1564
+ */
1565
+ enterprise: boolean;
615
1566
  /** Routing bucket of this account (`cn` | `ai`), derived from its credential. */
616
1567
  region: TraeRegion;
617
1568
  source: 'desktop' | 'dsh' | 'cli';
@@ -649,17 +1600,60 @@ declare function traeOwnAuthPath(region: TraeRegion): string;
649
1600
  */
650
1601
  declare function legacyTraeOwnAuthPath(): string;
651
1602
  declare function normalizeTraeCredential(raw: unknown, edition: TraeEdition, source: TraeCredential['source']): TraeCredential | undefined;
1603
+ /**
1604
+ * Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
1605
+ * pool.
1606
+ *
1607
+ * Evidence (2026-10-17, live): the tenant's own account carries
1608
+ * `account.tenant_id` / `tenant_name` and `scope: 'saas'`; the personal SOLO
1609
+ * account carries neither and reports `scope: 'marscode'`. Only the tenant one
1610
+ * streams on the enterprise gateway — the other answers `4019` risk control.
1611
+ */
1612
+ declare function isEnterpriseAccount(credential: Pick<TraeCredential, 'tenantId'> | undefined): boolean;
652
1613
  declare class TraeCredentialStore {
653
1614
  private storagePathOverride;
654
1615
  private edition;
655
1616
  private accountId;
656
1617
  private readonly region;
1618
+ private readonly requireTenant;
1619
+ private readonly hideTenantAccounts;
657
1620
  private readonly ownPathExplicit;
658
1621
  private readonly legacyOwnPath;
659
1622
  private readonly legacyOwnPathExplicit;
660
1623
  private readonly refresh;
661
1624
  private readonly refreshMarginMs;
662
1625
  private inflight;
1626
+ /**
1627
+ * In-flight refreshes of individual accounts, one per account id.
1628
+ *
1629
+ * The card reads every member's balance in one poll and each balance can
1630
+ * decide to refresh, so without this dedupe the same account would exchange
1631
+ * its token several times in a second and the last writer would win over a
1632
+ * copy another read already holds.
1633
+ */
1634
+ private readonly inflightByAccount;
1635
+ /**
1636
+ * When a per-account refresh last FAILED, so a dead refresh token is not
1637
+ * exchanged again on every poll.
1638
+ *
1639
+ * The card reads every member's balance on each refresh, and a revoked refresh
1640
+ * token fails forever: without this memo each poll would spend two upstream
1641
+ * calls (the current contract and the superseded one) on an account no retry
1642
+ * can fix. Five minutes keeps a transient network failure cheap to retry while
1643
+ * a revoked sign-in costs almost nothing.
1644
+ */
1645
+ private readonly refreshFailedAtMs;
1646
+ /**
1647
+ * The account the NEXT request should bill, when a pool — not the user's
1648
+ * binding — is choosing (issue #44).
1649
+ *
1650
+ * Runtime only, never persisted: the binding in settings is what a save
1651
+ * writes and what the card's account table reports as selected, while this is
1652
+ * "who serves right now". Keeping them apart is what lets the pool rotate
1653
+ * without silently rewriting the user's choice, and what makes turning the
1654
+ * pool off restore the bound account with no rebind at all.
1655
+ */
1656
+ private rotatedAccountId;
663
1657
  constructor(options: TraeCredentialStoreOptions);
664
1658
  /** Whether a credential's own claim belongs to this store's region. */
665
1659
  private matchesRegion;
@@ -713,7 +1707,80 @@ declare class TraeCredentialStore {
713
1707
  * binding itself already fell through to the live account.
714
1708
  */
715
1709
  liveLogin(): Promise<TraeAccountChoice | undefined>;
1710
+ /**
1711
+ * The accounts this store is willing to OFFER.
1712
+ *
1713
+ * Scoped to who this store may bill:
1714
+ * - a consumer store withholds tenant accounts (the enterprise pool is a
1715
+ * separate tab and a separate billing decision), and
1716
+ * - a tenant store withholds personal accounts (they cannot bill that
1717
+ * pool; the tenant gateway answers their chat with 4019).
1718
+ *
1719
+ * Either filter backs off when it would hide EVERYTHING, so a machine whose
1720
+ * only login is on the other side keeps working instead of reading as
1721
+ * "signed out".
1722
+ */
1723
+ private offered;
716
1724
  accounts(): Promise<TraeAccountChoice[]>;
1725
+ /**
1726
+ * The credential this store bills, given every credential it can see.
1727
+ *
1728
+ * This is the ONE resolution rule, and it exists so that `current()` and
1729
+ * `accounts()` cannot disagree: `current()` applies it to `readAll()`, and
1730
+ * `accounts()` applies the SAME verdict to decide which row reads as
1731
+ * `selected`. Deriving that flag independently is exactly how the two drifted
1732
+ * apart — and a card that marks a different account than the one the chat
1733
+ * bills is worse than no card at all, because a press that "saves the
1734
+ * selected account" then rewrites the binding to the account that was only
1735
+ * ever DISPLAYED.
1736
+ *
1737
+ * Order of business:
1738
+ * 1. No binding — the detected default (`preferred`) is in use, chosen from
1739
+ * the OFFERED list rather than from every login on the machine. A
1740
+ * consumer store that withholds tenant accounts must not default its
1741
+ * billing to the tenant pool merely because that login sorts first;
1742
+ * withholding is a billing decision, not a display detail.
1743
+ * 2. A binding — honour it verbatim, even when the OFFERED list hides that
1744
+ * account. Resolution is never scoped by `hideTenantAccounts`: the user's
1745
+ * explicit choice outranks a display filter.
1746
+ * 3. A tenant-scoped store cannot bill through a personal account, and the
1747
+ * tenant gateway only reveals that at the first chat (`4019`). Honour the
1748
+ * binding when it is eligible; otherwise fall back to the preferred
1749
+ * tenant account, which is the one the user's enterprise tab is meant to
1750
+ * use.
1751
+ * 4. A saved account can disappear when Trae replaces its local login. Do
1752
+ * NOT silently fall back to a different account: that would bill a
1753
+ * different account than the one the user explicitly selected. Return
1754
+ * undefined so the caller surfaces "no signed-in account" and the user
1755
+ * can re-select, instead of the plugin quietly switching accounts behind
1756
+ * their back.
1757
+ */
1758
+ private inUse;
1759
+ /**
1760
+ * Point this store at one specific account for the NEXT request, without
1761
+ * touching the user's saved binding (issue #44).
1762
+ *
1763
+ * Called by the account pool before each chat request. `undefined` clears the
1764
+ * override, which is how switching the pool off immediately restores the
1765
+ * binding — no rebind, no settings write, nothing to undo.
1766
+ *
1767
+ * A rotation is a REQUEST, not an assertion: `current()` honours it only when
1768
+ * the account still exists locally and this store's own eligibility rules
1769
+ * allow it, and otherwise falls back to the binding. So a rotation can never
1770
+ * invent a credential that {@link current} would refuse.
1771
+ */
1772
+ setRotatedAccount(accountId: string | undefined): void;
1773
+ /** The rotation currently in effect, for diagnostics and the pool's own card. */
1774
+ rotatedAccount(): string | undefined;
1775
+ /**
1776
+ * The credential a rotation points at, or `undefined` when it cannot be
1777
+ * honoured (the login was removed from this machine, or a tenant-scoped store
1778
+ * refuses a personal account).
1779
+ *
1780
+ * Never substitutes a different account here: the caller then applies the
1781
+ * user's binding, which is the honest answer when the pool's choice is gone.
1782
+ */
1783
+ private rotated;
717
1784
  current(): Promise<TraeCredential | undefined>;
718
1785
  /**
719
1786
  * The stored credential of one specific account, without changing which
@@ -731,6 +1798,22 @@ declare class TraeCredentialStore {
731
1798
  * fallback, for the same reason {@link current} refuses to fall back.
732
1799
  */
733
1800
  credentialOf(accountId: string): Promise<TraeCredential | undefined>;
1801
+ /**
1802
+ * A SPECIFIC account's credential, refreshed when its access token is due.
1803
+ *
1804
+ * {@link credentialOf} is a pure lookup and hands back an expired token as-is,
1805
+ * which is right for the chat path (that request fails honestly) but wrong for
1806
+ * a READ of that account's own data: Trae's access token lives for days while
1807
+ * the refresh token lives for months, so a pooled account the user has not
1808
+ * used this week reads as "no balance" — the card showed a bare `—` for an
1809
+ * account whose refresh token was still good. Only the refresh margin decides
1810
+ * here, and a FAILED refresh still yields the stored credential so the caller
1811
+ * reports the failure it actually hit instead of inventing "not signed in".
1812
+ *
1813
+ * This never touches the binding or the rotation: refreshing account B must
1814
+ * not make B the account the next chat request bills.
1815
+ */
1816
+ resolveCredentialOf(accountId: string): Promise<TraeCredential | undefined>;
734
1817
  /**
735
1818
  * Store imported credentials (issue #37), replacing any entry with the same
736
1819
  * account id and leaving every other entry alone.
@@ -1524,9 +2607,22 @@ interface TraeSoloClientOptions {
1524
2607
  credential(): Promise<TraeCredential>;
1525
2608
  identity(): Promise<TraeIdentity>;
1526
2609
  baseUrl?: string;
2610
+ /**
2611
+ * Which chat gateway this client is pinned to. Omitted = `solo`, the consumer
2612
+ * pool. `enterprise` targets the tenant-billed `console.enterprise` gateway
2613
+ * and stamps the per-call `session_id` it requires (see
2614
+ * {@link TRAE_ENTERPRISE_HOST}).
2615
+ *
2616
+ * Pinned per CLIENT, never chosen per call: a turn must not be able to drift
2617
+ * from the pool the user picked, so the route is fixed when the provider is
2618
+ * built and every request that client makes stays on it.
2619
+ */
2620
+ route?: TraeChatRoute;
1527
2621
  fetchImpl?: typeof fetch;
1528
2622
  log?: (message: string, detail?: unknown) => void;
1529
2623
  }
2624
+ /** Which chat pool a client is pinned to. */
2625
+ type TraeChatRoute = 'solo' | 'enterprise';
1530
2626
  declare class TraeSoloUpstreamClient {
1531
2627
  private readonly options;
1532
2628
  private readonly fetchImpl;
@@ -1547,9 +2643,22 @@ declare class TraeSoloUpstreamClient {
1547
2643
  /** Merge one function's config list into the shared catalogue (first wins). */
1548
2644
  private collectModels;
1549
2645
  chatStream(bodyJson: string, signal?: AbortSignal, functionName?: string): Promise<TraeChatResult>;
2646
+ /** Chat gateway for this client's pinned route. */
2647
+ private chatBaseOf;
1550
2648
  }
1551
2649
  //#endregion
1552
2650
  //#region src/solo-bridge.d.ts
2651
+ /**
2652
+ * One upstream failure as Trae reports it inside a 200 SSE body: an `error`
2653
+ * event, or any event carrying a `code` at or above Trae's error band.
2654
+ */
2655
+ interface TraeFailure {
2656
+ code?: number;
2657
+ message: string;
2658
+ extra?: unknown;
2659
+ }
2660
+ /** A readable message for an in-stream Trae failure, with its code preserved. */
2661
+ declare function describeTraeFailure(failure: TraeFailure): string;
1553
2662
  /** Convert Trae's named SSE events into OpenAI chat-completion SSE chunks. */
1554
2663
  declare function bridgeTraeSoloStream(response: Response, model: string): Response;
1555
2664
  /**
@@ -1790,573 +2899,307 @@ declare class TraeUsageClient {
1790
2899
  payStatus(signal?: AbortSignal): Promise<TraePayStatus>;
1791
2900
  /**
1792
2901
  * The Work-credit endpoints are CN-only. The international region is
1793
- * subscription-based and must read {@link payStatus} instead; guarding here
1794
- * keeps the card's degraded `creditsError` message diagnosable rather than
1795
- * letting an ai credential hit an unverified path on its pay gateway.
1796
- */
1797
- private requireCnRegion;
1798
- /** Total entitlements / credits and per-pack breakdown. */
1799
- snapshot(signal?: AbortSignal): Promise<TraeUsageSnapshot>;
1800
- /**
1801
- * Daily check-in status. Read-only, and answered with or without the device
1802
- * header — the claim is what needs it.
1803
- *
1804
- * The read DOES carry the header whenever this installation has one, and
1805
- * that is deliberate rather than incidental: `did_checked_in` is answered
1806
- * per device, so omitting the header would make a device that already claimed
1807
- * today look identical to one that never has (measured 2026-09-26, see
1808
- * {@link TraeCheckinStatus.didCheckedIn}).
1809
- */
1810
- checkinStatus(signal?: AbortSignal): Promise<TraeCheckinStatus>;
1811
- /**
1812
- * Claim today's check-in reward. The ONLY state-changing call in this client.
1813
- *
1814
- * The upstream is idempotent per Beijing day (verified 2026-09-24: repeating
1815
- * the call on an already-claimed day answers `code: 0` while the entitlement
1816
- * total stays byte-identical), so a double click cannot double-grant. The
1817
- * card and its route still guard, because "cannot double-grant" is a property
1818
- * of the upstream we verify rather than one we rely on.
1819
- *
1820
- * A business refusal arrives as HTTP 200 with a non-zero `code` — most often
1821
- * `9004` when the request carries no `x-device-id`. That is reported as
1822
- * `claimed: false` rather than thrown, so the caller can tell "the upstream
1823
- * refused this" apart from "the request never arrived".
1824
- */
1825
- claimCheckin(signal?: AbortSignal): Promise<TraeCheckinClaim>;
1826
- /** Rewards / activity rules. */
1827
- activities(signal?: AbortSignal): Promise<TraeActivityRule[]>;
1828
- /** Convenience: snapshot + check-in + activities in one call (best-effort, non-fatal on missing). */
1829
- view(signal?: AbortSignal): Promise<TraeUsageView>;
1830
- }
1831
- //#endregion
1832
- //#region src/status-paths.d.ts
1833
- /** Plugin-owned usage endpoint consumed by its browser half. */
1834
- declare const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
1835
- /**
1836
- * Plugin-owned availability probe: POST the model ids to test, get one verdict
1837
- * per model. Unlike every read-only sibling this route SPENDS quota — a real
1838
- * (minimal) chat call per model — so it is user-initiated only, and the card
1839
- * labels the button as consuming a small amount.
1840
- */
1841
- declare const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
1842
- /**
1843
- * Per-account status (check-in / credits / subscription) for the account table.
1844
- *
1845
- * Each row of the account table reads its OWN state through this route — a CN
1846
- * row carries its check-in state and balance, an ai row its subscription.
1847
- * POST with the accountId in the body: one upstream read per account, paid
1848
- * only when the table is actually shown.
1849
- */
1850
- declare const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
1851
- /**
1852
- * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
1853
- * route in this plugin that changes upstream account state. The accountId in
1854
- * the body names WHICH account to claim for, so the button can sit in each
1855
- * account row.
1856
- */
1857
- declare const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
1858
- /** Query parameter naming the region a card request addresses. */
1859
- declare const TRAE_REGION_PARAM = "region";
1860
- /** Every region, in card tab order. */
1861
- declare const TRAE_REGIONS: readonly TraeRegion[];
1862
- /**
1863
- * Address one region's status route. The two regions are separate provider
1864
- * stacks; every card request carries the region whose tab the user is on.
1865
- */
1866
- declare function withTraeRegion(path: string, region: TraeRegion): string;
1867
- /**
1868
- * Read the region parameter off a status-route URL. Absent means the domestic
1869
- * tab (`cn`); a present-but-unknown value returns undefined so the route can
1870
- * answer 400 instead of guessing.
1871
- */
1872
- declare function regionOfTraeStatusUrl(url: string): TraeRegion | undefined;
1873
- /** One credit pack and its remaining credit. */
1874
- interface TraeWebCreditAccount {
1875
- displayDesc: string;
1876
- remain: number;
1877
- size: number;
1878
- /**
1879
- * When this pack lapses, in epoch milliseconds, taken from the upstream's own
1880
- * `end_time`. Absent when the upstream reported no usable time (0 / missing),
1881
- * so a consumer can tell "no date" apart from "1970".
1882
- */
1883
- expiresAtMs?: number;
1884
- }
1885
- /** Credit lapsing inside one horizon, as the card reports it. */
1886
- interface TraeWebExpiringCredits {
1887
- /** Sum of the contributing packs' remaining credit. */
1888
- total: number;
1889
- /** How many packs contribute. */
1890
- packs: number;
1891
- /** Soonest expiry among them, epoch milliseconds. */
1892
- earliestExpiresAtMs?: number;
1893
- }
1894
- /**
1895
- * Credit at risk, bucketed by how soon it lapses.
1896
- *
1897
- * Only present when at least one pack carried a usable `expiresAtMs` — a card
1898
- * must never read "we were not told when anything expires" as "nothing is
1899
- * about to lapse".
1900
- */
1901
- interface TraeWebCreditExpiry {
1902
- within24h: TraeWebExpiringCredits;
1903
- within3d: TraeWebExpiringCredits;
1904
- within7d: TraeWebExpiringCredits;
1905
- /** Packs already past their end time but still carrying credit. */
1906
- expired: TraeWebExpiringCredits;
1907
- }
1908
- /** Aggregated usage/credit answer rendered by the plugin card. */
1909
- interface TraeWebCredits {
1910
- total: number;
1911
- consumed: number;
1912
- available: number;
1913
- workAvailable: number;
1914
- generalAvailable: number;
1915
- accounts: readonly TraeWebCreditAccount[];
1916
- /**
1917
- * Credit lapsing soon, for the card's warning banner.
1918
- *
1919
- * Derived from the per-pack {@link TraeWebCreditAccount.expiresAtMs} the
1920
- * `/usage` route already carries, so it costs no extra upstream read.
1921
- */
1922
- expiry?: TraeWebCreditExpiry;
1923
- }
1924
- /** Editable Trae model row rendered by the plugin-owned settings card. */
1925
- interface TraeWebModel {
1926
- id: string;
1927
- name: string;
1928
- contextWindow?: number;
1929
- maxTokens?: number;
1930
- input?: ('text' | 'image')[];
1931
- creditMultiplier?: number;
1932
- reasoningSupported?: boolean;
1933
- reasoning?: {
1934
- supported: string[];
1935
- defaultEffort?: string;
1936
- };
1937
- maxContextWindow?: number;
2902
+ * subscription-based and must read {@link payStatus} instead; guarding here
2903
+ * keeps the card's degraded `creditsError` message diagnosable rather than
2904
+ * letting an ai credential hit an unverified path on its pay gateway.
2905
+ */
2906
+ private requireCnRegion;
2907
+ /** Total entitlements / credits and per-pack breakdown. */
2908
+ snapshot(signal?: AbortSignal): Promise<TraeUsageSnapshot>;
1938
2909
  /**
1939
- * Trae's own image-capability flag, as the runtime catalog carries it. The
1940
- * card renders these rows with their image checkbox already ticked so the
1941
- * control matches what DSH is actually told (issue #16); absent means the
1942
- * directory did not declare it, which is not a capability claim either way.
2910
+ * Daily check-in status. Read-only, and answered with or without the device
2911
+ * header — the claim is what needs it.
2912
+ *
2913
+ * The read DOES carry the header whenever this installation has one, and
2914
+ * that is deliberate rather than incidental: `did_checked_in` is answered
2915
+ * per device, so omitting the header would make a device that already claimed
2916
+ * today look identical to one that never has (measured 2026-09-26, see
2917
+ * {@link TraeCheckinStatus.didCheckedIn}).
1943
2918
  */
1944
- multimodal?: boolean;
2919
+ checkinStatus(signal?: AbortSignal): Promise<TraeCheckinStatus>;
1945
2920
  /**
1946
- * The availability verdict from the one-click test, when this card session
1947
- * has run one. Absent = not tested. Deliberately NOT persisted with the
1948
- * settings: it describes one moment on one account, and a stale verdict shown
1949
- * as current is worse than a blank field.
2921
+ * Claim today's check-in reward. The ONLY state-changing call in this client.
2922
+ *
2923
+ * The upstream is idempotent per Beijing day (verified 2026-09-24: repeating
2924
+ * the call on an already-claimed day answers `code: 0` while the entitlement
2925
+ * total stays byte-identical), so a double click cannot double-grant. The
2926
+ * card and its route still guard, because "cannot double-grant" is a property
2927
+ * of the upstream we verify rather than one we rely on.
2928
+ *
2929
+ * A business refusal arrives as HTTP 200 with a non-zero `code` — most often
2930
+ * `9004` when the request carries no `x-device-id`. That is reported as
2931
+ * `claimed: false` rather than thrown, so the caller can tell "the upstream
2932
+ * refused this" apart from "the request never arrived".
1950
2933
  */
1951
- probe?: TraeModelProbeVerdict;
1952
- /** Upstream's own words for the verdict, when it gave any. */
1953
- probeDetail?: string;
2934
+ claimCheckin(signal?: AbortSignal): Promise<TraeCheckinClaim>;
2935
+ /** Rewards / activity rules. */
2936
+ activities(signal?: AbortSignal): Promise<TraeActivityRule[]>;
2937
+ /** Convenience: snapshot + check-in + activities in one call (best-effort, non-fatal on missing). */
2938
+ view(signal?: AbortSignal): Promise<TraeUsageView>;
1954
2939
  }
2940
+ //#endregion
2941
+ //#region src/account-pool.d.ts
1955
2942
  /**
1956
- * One model's availability verdict from the one-click test.
2943
+ * Which of the pool's writers produced a measurement.
1957
2944
  *
1958
- * `unknown` is a first-class outcome, not a failure: a timeout, a proxy drop or
1959
- * a throttle says nothing about the model, and reporting it as unusable would
1960
- * make the user delete a model that works. Only an upstream refusal is negative.
2945
+ * `test-batch` — the user pressed "test" (or the check-in batch), so the outcome
2946
+ * is a deliberate measurement of the target model.
2947
+ * `live-request` — a real chat request failed and its failure was recorded so the
2948
+ * NEXT request starts from a measurement instead of rediscovering the same wall.
2949
+ *
2950
+ * The distinction is diagnostic, not behavioural: both feed the same routing and
2951
+ * cooling logic. It exists so a user who watched an account measure `ok` and
2952
+ * later saw it limited can tell whether that was a later live failure (expected)
2953
+ * or an older value winning (a bug).
1961
2954
  */
1962
- type TraeModelProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
1963
- /** What the probe route answers with. */
1964
- interface TraeModelsTestResponse {
1965
- /** One entry per model tested, in the order they were sent. */
1966
- results: {
1967
- id: string;
1968
- verdict: TraeModelProbeVerdict;
1969
- detail: string;
1970
- }[];
1971
- /** Models skipped because the region has no live catalog row for them. */
1972
- skipped?: string[];
2955
+ type TraePoolProbeSource = 'test-batch' | 'live-request';
2956
+ /** One account's pool entry: what the picker needs, plus its last measurement. */
2957
+ interface TraePoolAccount {
2958
+ /** Stable account id (the same one `TraeWebAccount` carries). */
2959
+ id: string;
2960
+ /** Human name, or `''` when the desktop app recorded none. */
2961
+ accountName: string;
1973
2962
  }
1974
2963
  /**
1975
- * Daily check-in state rendered below the credit stats.
1976
- *
1977
- * The two booleans answer DIFFERENT questions and must never be collapsed into
1978
- * one "done" flag — doing that was a shipped bug (see CHANGELOG 2.3.1):
1979
- *
1980
- * - `checkedIn` is ACCOUNT-level: today's reward pack exists for this account,
1981
- * so there is nothing left to claim and the balance already includes it.
1982
- * - `didCheckedIn` is DEVICE-level: this MACHINE already spent its one
1983
- * check-in for the day, whichever account spent it. The upstream keys it on
1984
- * the `x-device-id` in the request and answers it per device, not per
1985
- * account (measured 2026-09-26: the same account and Beijing day answered
1986
- * `false` with the header omitted, with a synthetic id, and with another
1987
- * install's id, and `true` for the device that actually claimed).
2964
+ * What the pool knows about ONE account's last test of the target model.
1988
2965
  *
1989
- * So `didCheckedIn && !checkedIn` is the "switched account" case: the device's
1990
- * daily check-in is used up, but THIS account was never rewarded. Claiming
1991
- * again is refused upstream (business code 9095, "该设备今日已参与签到"), so
1992
- * the card disables the button — but it must say that, not "claimed today".
2966
+ * `undefined` means "never tested", which is deliberately distinct from every
2967
+ * measured outcome: an untested account is a candidate (the user may want to use
2968
+ * it now) whereas a measured failure is not. Collapsing the two would make a
2969
+ * fresh pool look empty until the first test.
1993
2970
  */
1994
- interface TraeWebCheckin {
1995
- checkedIn: boolean;
1996
- didCheckedIn: boolean;
1997
- credits: number;
1998
- enabled: boolean;
1999
- extraCredits?: number;
2000
- }
2001
- /** The claim route's answer, so the card can report a business refusal verbatim. */
2002
- interface TraeWebCheckinClaim {
2003
- /** False when the upstream answered a non-zero business code (e.g. 9004). */
2004
- claimed: boolean;
2971
+ interface TraePoolProbe {
2972
+ outcome: TraePoolOutcome;
2973
+ /** When this measurement was taken, in epoch ms. */
2974
+ atMs: number;
2975
+ /** WHAT WROTE this measurement; absent for records written before it existed. */
2976
+ source?: TraePoolProbeSource;
2005
2977
  /**
2006
- * True only when THIS ACCOUNT's reward for today already exists, so nothing
2007
- * was sent upstream. Distinct from {@link TraeWebCheckinClaim.deviceCheckedIn}:
2008
- * a device that already claimed for another account leaves this false.
2978
+ * When this account may be usable again, when the upstream stated a time.
2979
+ *
2980
+ * Present for a `rate-limited` / `out-of-credit` outcome whose failure carried
2981
+ * a reset sentence. ABSENT when the upstream named no time — that absence is
2982
+ * meaningful and must never be turned into a locally invented countdown.
2009
2983
  */
2010
- alreadyCheckedIn: boolean;
2984
+ retryAtMs?: number;
2011
2985
  /**
2012
- * True when today's check-in was already spent on this DEVICE (by this or
2013
- * another account), so no claim was sent or the upstream refused it with
2014
- * 9095. Not an error: it is the truthful answer to "can this account claim
2015
- * here today?" — no, and the reason is the machine, not the account.
2986
+ * The upstream's own words on the failure, already redacted.
2987
+ *
2988
+ * Kept so the card can say WHY an account left the pool instead of repeating
2989
+ * one generic sentence for a DNS failure, a gateway 502 and a cancelled request
2990
+ * alike.
2016
2991
  */
2017
- deviceCheckedIn?: boolean;
2018
- code?: number;
2019
2992
  message?: string;
2020
- /** The refreshed check-in state, present whenever the claim path ran. */
2021
- checkin?: TraeWebCheckin;
2022
- }
2023
- /** One reward activity rule (name + work/general credits when present). */
2024
- interface TraeWebActivity {
2025
- activityId: string;
2026
- enabled: boolean;
2027
- workCredits?: number;
2028
- generalCredits?: number;
2029
- }
2030
- interface TraeWebAccount {
2031
- id: string;
2032
- accountName: string;
2033
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2034
- /** Routing bucket of this account, derived from its credential. */
2035
- region: TraeRegion;
2036
- source: 'desktop' | 'dsh' | 'cli';
2037
- tokenExpiresAtMs: number;
2038
2993
  /**
2039
- * When this account's REFRESH token expires, when the credential carries one.
2994
+ * WHICH MODEL this measurement is about.
2040
2995
  *
2041
- * The access token above is renewed automatically — the plugin refreshes it
2042
- * five minutes before it lapses — so by itself it answers "when does this
2043
- * need attention", which is "never, while the refresh token lives". The
2044
- * refresh token is the real horizon: once IT expires the automatic renewal
2045
- * stops working and the user must sign in to Trae again. Before this field the
2046
- * card could only say "auto-renewal", which stays true right up to the moment
2047
- * it silently stops being true.
2996
+ * `1005` (plan gate), `4001` (no SOLO function serves this `config_name`) and
2997
+ * `4011` (throttle) are all ACCOUNT × MODEL conditions: the same credential
2998
+ * answers other models at the same second. A row rendered as plain "可用"
2999
+ * therefore claims more than the measurement says. Storing the model id is what
3000
+ * makes the claim checkable.
3001
+ *
3002
+ * Deliberately NOT inherited when an update omits it — the opposite of
3003
+ * `accountName` and `retryAtMs`. Those describe the ACCOUNT (still true when a
3004
+ * later writer cannot see them); the model describes THIS measurement, and
3005
+ * carrying an older id forward would relabel a fresh measurement with a model
3006
+ * it was not taken on.
2048
3007
  */
2049
- refreshExpiresAtMs?: number;
2050
- selected: boolean;
2051
- }
2052
- /**
2053
- * The account the Trae desktop app is signed in to right now, when it differs
2054
- * from the one this plugin is bound to (issue #29).
2055
- *
2056
- * Reported on the usage document so the card can offer one-click follow without
2057
- * an extra round trip; also written back on a POST to the accounts refresh
2058
- * route when the client confirms it saw the hint. Absent whenever the two
2059
- * agree or the app is not readable — a healthy document carries no extra
2060
- * fields and the plugin never bills from this value directly: the FOLLOW is a
2061
- * settings write of `accounts.<region>`, made by the card or by the host's
2062
- * follow switch, never a silent rebind here.
2063
- */
2064
- interface TraeWebAppLogin {
2065
- id: string;
2066
- accountName: string;
2067
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2068
- region: TraeRegion;
2069
- }
2070
- /**
2071
- * The per-account state shown inside an account row of the card's table.
2072
- *
2073
- * Every locally detected account in a region is rendered as a row; the
2074
- * interesting per-account facts differ by region, which is why this is a union
2075
- * of the three surfaces the card previously showed only for the BOUND account:
2076
- *
2077
- * - CN rows carry their own check-in state (Trae keys the daily reward per
2078
- * ACCOUNT, and the one-per-device tally per DEVICE, see {@link TraeWebCheckin})
2079
- * and their Work/General balances.
2080
- * - ai rows carry their own subscription / trial status.
2081
- *
2082
- * Each field is optional: a read that fails keeps the row visible (perhaps
2083
- * without a figure) rather than dropping the account and its switch.
2084
- */
2085
- interface TraeWebAccountState {
2086
- /** The account this state belongs to. */
2087
- id: string;
2088
- /** CN: today's check-in state of this account (absent for ai rows). */
2089
- checkin?: TraeWebCheckin;
2090
- /** CN: this account's Work/General balances (absent for ai rows). */
2091
- credits?: TraeWebCredits;
2092
- /** ai: this account's subscription / trial status (absent for CN rows). */
2093
- payStatus?: TraeWebPayStatus;
3008
+ modelId?: string;
3009
+ /**
3010
+ * The account's display name AT THE MOMENT IT WAS MEASURED.
3011
+ *
3012
+ * Stored because a saved pool member can outlive its sign-in: once the desktop
3013
+ * app drops a login, that id resolves to no listed account and the card has
3014
+ * nothing left to render for it. The name was in hand at write time and nowhere
3015
+ * afterwards. Snapshots are never refreshed from a LISTING — only a real
3016
+ * measurement writes this.
3017
+ */
3018
+ accountName?: string;
2094
3019
  }
2095
- interface TraeWebCreditAlternative {
3020
+ /** One account's live credit situation, as the pool ranks it. */
3021
+ interface TraePoolCredits {
3022
+ /** Remaining credit across every pack (`available` of the upstream snapshot). */
3023
+ total: number;
3024
+ /** The Work bucket, kept for display: SOLO chat does NOT spend it. */
3025
+ workAvailable?: number;
3026
+ /** The General bucket, kept for display: this is what SOLO chat spends. */
3027
+ generalAvailable?: number;
3028
+ /** Credits expiring within the card's "soon" window (3 days). */
3029
+ expiringSoon: number;
3030
+ /** When the nearest at-risk pack expires, if any does. */
3031
+ nearestExpiryMs?: number;
3032
+ }
3033
+ /**
3034
+ * Everything the ranking rule reads about one account. Assembled by the Host from
3035
+ * the credential store, the per-account credit read, and the runtime probe store
3036
+ * — this module only consumes it.
3037
+ */
3038
+ interface TraePoolMember {
3039
+ account: TraePoolAccount;
3040
+ credits?: TraePoolCredits;
3041
+ probe?: TraePoolProbe;
3042
+ /** When the stored credential expires, used as the final tie-break. */
3043
+ tokenExpiresAtMs?: number;
3044
+ }
3045
+ /**
3046
+ * How one account answered the target model.
3047
+ *
3048
+ * The first nine values are the workbuddy taxonomy verbatim so the two cards read
3049
+ * the same. The tenth, `plan-gated`, is Trae-only: upstream `1005` says THIS
3050
+ * account's plan does not cover this model while another account's plan may, and
3051
+ * no amount of waiting changes a plan — so it must not be folded into
3052
+ * `credential-rejected` (which would demand a re-login) nor into
3053
+ * `policy-rejected` (which means a content filter tripped on ONE request).
3054
+ */
3055
+ type TraePoolOutcome = 'ok' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'unavailable' | 'not-found' | 'failed';
3056
+ //#endregion
3057
+ //#region src/model-probe.d.ts
3058
+ /** How one model answered. */
3059
+ type TraeProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
3060
+ interface TraeProbeResult {
3061
+ /** The model id as the card knows it. */
2096
3062
  id: string;
2097
- accountName: string;
2098
- edition: TraeWebAccount['edition'];
2099
- workAvailable: number;
2100
- generalAvailable: number;
2101
- }
2102
- /** One probed candidate path and why it did not yield an account. */
2103
- interface TraeWebSearchPath {
2104
- path: string;
2105
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2106
- source: 'desktop' | 'cli';
2107
- reason: 'missing' | 'unreadable' | 'invalid';
2108
- message?: string;
2109
- }
2110
- /**
2111
- * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
2112
- * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
2113
- */
2114
- declare function unwrapVolatile<T>(value: T): T;
2115
- /**
2116
- * Deep copy of a value with every `{get(): T}` live reference replaced by the
2117
- * value it resolves to.
2118
- *
2119
- * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
2120
- * reference is still `typeof === 'object'` — so it passes a naive object check
2121
- * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
2122
- * does NOT read the field: it produces `{ get: <function> }`. Any merge that
2123
- * spreads a resolved field to preserve its siblings — the card's "write one
2124
- * region, keep the other" write, or the account map's spread — would otherwise
2125
- * DROP every sibling and leak a function into the document.
2126
- *
2127
- * Both halves therefore unwrap before touching a resolved field. This lives in
2128
- * the node-free host↔client bridge because the browser half needs it too.
2129
- *
2130
- * Non-reference values are recursed into so a nested volatile field is caught
2131
- * as well; arrays and objects are rebuilt rather than mutated, so the caller's
2132
- * value is never touched.
2133
- */
2134
- declare function unwrapVolatileDeep<T>(value: T): T;
2135
- /**
2136
- * Read the per-region account selections out of a settings value.
2137
- *
2138
- * SHARED because `accounts` now has TWO writers: the card, and the composer
2139
- * panel's account table (2.14.0). Two writers of one field must use one merge
2140
- * rule, or they drift — and the rule is not obvious.
2141
- *
2142
- * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
2143
- * live reference, which passes the `typeof === 'object'` check and would be
2144
- * returned as if it were the map. Every lookup on it is then `undefined`, and
2145
- * the merge (`{ ...configured, [region]: id }`) would spread a reference into
2146
- * `{get: <function>}` — dropping the other region's selection and leaking a
2147
- * function into the settings document.
2148
- */
2149
- declare function configuredAccountsOf(configured: unknown): Record<string, string>;
2150
- /**
2151
- * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
2152
- * #29). Absent reads as OFF, so an existing configuration keeps the strict
2153
- * bind it shipped with and the switch is an opt-in — the same opt-out
2154
- * semantics as {@link regionEnabledOf}, for the same reason: a config written
2155
- * before this field existed cannot ask for behavior it never declared.
2156
- *
2157
- * Reads the whole settings section (or the bare field); deep-unwraps first
2158
- * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
2159
- * reference, whose `typeof === 'object'` would otherwise pass a naive check.
2160
- */
2161
- declare function followAppLoginEnabled(configured: unknown): boolean;
3063
+ verdict: TraeProbeVerdict;
3064
+ /** Upstream's own words (already translated), or a short note. */
3065
+ detail: string;
3066
+ }
2162
3067
  /**
2163
- * Build the next `regions` settings value for the card's save. The write
2164
- * targets ONLY the signed-in account's region slot; every other region's slot
2165
- * is carried over untouched, so switching accounts never clobbers the other
2166
- * region's picks. Tolerates any stored shape (absent, non-object) by starting
2167
- * from an empty document.
3068
+ * How long one probe may take. Trae answers a one-word prompt in a few seconds;
3069
+ * beyond this the answer is not worth waiting for and is reported as unknown.
2168
3070
  */
2169
- declare function nextRegionSlots<Slot extends object>(regions: unknown, region: TraeRegion, slot: Slot): Record<string, unknown>;
3071
+ declare const TRAE_PROBE_TIMEOUT_MS = 30000;
2170
3072
  /**
2171
- * Whether one region's provider is switched on. Opt-out semantics: only an
2172
- * explicit `false` disables it, so a config written before this switch existed
2173
- * (and the pre-region-split flat fields, which never carry `enabled`) keep both
2174
- * providers running exactly as before. The Host reads the same rule through
2175
- * `regionStateOf`, so card and Host can never disagree about a region's state.
3073
+ * Classify a failure that arrived as a thrown stream error rather than a
3074
+ * returned result.
2176
3075
  *
2177
- * `value` may be the whole settings section or the `regions` map (see
2178
- * {@link regionsMapOf}).
3076
+ * The bridge errors the stream (instead of returning a result) when a refusal
3077
+ * interrupts an answer that had already started; its message is the same
3078
+ * translated text, so a named code is still decisive. Anything without a
3079
+ * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
2179
3080
  */
2180
- declare function regionEnabledOf(value: unknown, region: TraeRegion): boolean;
3081
+ declare function classifyProbeError(message: string): {
3082
+ verdict: TraeProbeVerdict;
3083
+ detail: string;
3084
+ };
2181
3085
  /**
2182
- * Build the next `regions` settings value for a provider on/off toggle. ONLY
2183
- * the target region's `enabled` flag changes: every other field of that slot
2184
- * (its directory, selection, image opt-ins, context budgets) and every other
2185
- * region's slot are carried over verbatim, so switching a provider off never
2186
- * discards the user's model picks and switching it back on restores them.
2187
- *
2188
- * This is deliberately separate from {@link nextRegionSlots}: that helper
2189
- * writes a whole slot from a signed-in tab's draft, while this one must work
2190
- * for a region that is signed OUT — which is precisely the region a user wants
2191
- * to switch off (no international install, no international account).
3086
+ * Classify a completed upstream call.
2192
3087
  *
2193
- * `value` may be the whole settings section or the `regions` map; the RETURN
2194
- * value is always the `regions` map, i.e. exactly what `settingsScope.set(
2195
- * 'regions', ...)` needs.
3088
+ * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
3089
+ * arrived inside a 200 body has already been turned into a real error result
3090
+ * (and its code translated into `message`) by the bridge — this function only
3091
+ * has to read it back.
2196
3092
  */
2197
- declare function nextRegionEnabled(value: unknown, region: TraeRegion, enabled: boolean): Record<string, unknown>;
3093
+ declare function classifyProbeResult(result: TraeChatResult, outputSeen: boolean): {
3094
+ verdict: TraeProbeVerdict;
3095
+ detail: string;
3096
+ };
2198
3097
  /**
2199
- * The two seats this region's credit readout can occupy.
2200
- *
2201
- * `composer` is the composer tool row (`conversation.input.left`); `stats` is
2202
- * the ambient dock below the composer card (`conversation.composer.dock`),
2203
- * beside the shell's own round/step/token pills.
3098
+ * The chat call one probe makes.
2204
3099
  *
2205
- * WHY THE SEAT IS A CHOICE AT ALL (learned from dsh-connect-workbuddy issue
2206
- * #62): the composer tool row is a LIST seat, so it is shared not only with the
2207
- * shell's own controls but with any other plugin that registers there — and a
2208
- * list seat cannot promise that every occupant is visible. The report behind
2209
- * that issue is exactly that: with a third-party plugin installed the readout
2210
- * stopped appearing. A fixed seat therefore lets one plugin's presence decide
2211
- * another's, so the seat belongs to the user.
3100
+ * `inputTokens` pads the user turn so the probe carries roughly as much text as
3101
+ * a real conversation. Size matters because an upstream that refuses LARGE
3102
+ * requests will happily answer a one-word probe, and a verdict earned by a
3103
+ * 1-token request says nothing about the 30k-token turn the user is about to
3104
+ * send. `0` keeps the historical minimal probe (one word) — that is the default
3105
+ * because it spends the least credit and answers the question the pool ranks on
3106
+ * ("can this account talk at all").
2212
3107
  *
2213
- * Named as a union rather than a boolean because a third seat is plausible and
2214
- * a `showAtTop: boolean` would have to be MIGRATED rather than extended.
3108
+ * The filler is `x` words: one ASCII word is about one token, so the body size
3109
+ * is predictable (tokens ≈ inputTokens × 2 characters) without a tokenizer.
2215
3110
  */
2216
- type TraeCreditPosition = 'composer' | 'stats';
2217
3111
  /**
2218
- * Which seat one region's readout uses, defaulting to the composer row.
3112
+ * The largest probe this plugin will pad.
2219
3113
  *
2220
- * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
2221
- * the two cases that matter: a profile written BEFORE this setting existed (the
2222
- * composer row was the only seat, so an upgrade moves nothing on screen), and a
2223
- * hand-edited value the schema does not know. Falling back rather than
2224
- * rendering nowhere is deliberate — an unrecognized value must not make the
2225
- * readout disappear, which is the very defect this setting exists to fix.
2226
- *
2227
- * `value` may be the whole settings section or the `regions` map (see
2228
- * {@link regionsMapOf}).
3114
+ * A 100k-token probe is already ~200 KB of JSON; anything above that is a request
3115
+ * the user's real turn would not plausibly reach in this product, and a config
3116
+ * file edited by hand must not turn one "test all" click into a multi-megabyte
3117
+ * upload.
2229
3118
  */
2230
- declare function regionCreditPositionOf(value: unknown, region: TraeRegion): TraeCreditPosition;
3119
+ declare const MAX_PROBE_INPUT_TOKENS = 100000;
3120
+ declare function probeBody(model: string, inputTokens?: number): string;
2231
3121
  /**
2232
- * Build the next `regions` settings value for one region's credit seat.
2233
- *
2234
- * Same merge rule as every sibling region write: only the target region's
2235
- * `creditPosition` changes, so a seat change cannot drop that slot's model
2236
- * directory, selection, budgets or enabled flag, nor touch the other region.
3122
+ * Probe one model through the bridge the plugin actually chats with, so the
3123
+ * recorded verdict describes the user's real path (same wire resolution, same
3124
+ * function stamping, same error translation).
3125
+ */
3126
+ declare function probeOneModel(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, id: string, timeoutMs?: number, inputTokens?: number): Promise<TraeProbeResult>;
3127
+ /**
3128
+ * Probe a list of models ONE AT A TIME.
2237
3129
  *
2238
- * `value` may be the whole settings section or the `regions` map; the RETURN
2239
- * value is always the `regions` map, i.e. exactly what `settingsScope.set(
2240
- * 'regions', ...)` needs.
3130
+ * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
3131
+ * would manufacture the very `rate_limited`/`unknown` verdicts this feature
3132
+ * must avoid. An authentication failure stops the run — every later probe would
3133
+ * fail the same way, and spending calls to learn that again is waste.
2241
3134
  */
2242
- declare function nextRegionCreditPosition(value: unknown, region: TraeRegion, position: TraeCreditPosition): Record<string, unknown>;
2243
- /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
2244
- interface TraeWebPayStatus {
2245
- isDollarUsageBilling: boolean;
2246
- hasPackage: boolean;
2247
- isPayFreshman: boolean;
2248
- inTrial: boolean;
2249
- trialEndTimeMs: number;
2250
- enableSoloLite: boolean;
2251
- enableSoloBuilder: boolean;
2252
- enableSoloCoder: boolean;
2253
- enableSoloWeb: boolean;
2254
- /** Trae's authoritative tier number (`user_pay_identity`); `0` means Free. */
2255
- payIdentity?: number;
2256
- /** Trae's authoritative tier label (`user_pay_identity_str`), e.g. `Free`. */
2257
- payIdentityStr?: string;
2258
- fission?: {
2259
- startTimeMs: number;
2260
- expireTimeMs: number;
2261
- maxUsage: number;
2262
- };
2263
- }
2264
- /** The JSON document the plugin card renders. */
2265
- type TraeWebUsage = {
2266
- status: 'signed-out';
2267
- accounts: readonly TraeWebAccount[];
2268
- message?: string;
2269
- searched?: readonly TraeWebSearchPath[];
2270
- enabled?: boolean;
2271
- } | {
2272
- status: 'signed-in';
3135
+ declare function probeModelsSequentially(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, ids: readonly string[], onResult?: (result: TraeProbeResult, index: number) => void): Promise<TraeProbeResult[]>;
3136
+ //#endregion
3137
+ //#region src/account-pool-run.d.ts
3138
+ /** One account's check-in outcome. */
3139
+ interface TraePoolCheckinRow {
2273
3140
  accountId: string;
2274
3141
  accountName: string;
2275
- tokenExpiresAtMs: number;
2276
- /**
2277
- * When the BOUND account's refresh token expires (absent when the credential
2278
- * carries none). See {@link TraeWebAccount.refreshExpiresAtMs} — this is the
2279
- * horizon that actually matters, because the access token above renews
2280
- * itself until this one lapses.
2281
- */
2282
- refreshExpiresAtMs?: number;
2283
- /** Which per-region model directory and selection this account owns. */
2284
- region: TraeRegion;
2285
- /** Whether this region's provider is currently offered to DSH. */
2286
- enabled?: boolean;
2287
- accounts: readonly TraeWebAccount[];
2288
- models: readonly TraeWebModel[];
2289
- enabledModelIds: readonly string[];
2290
- rawChat?: {
2291
- state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
2292
- checkedAtMs?: number;
2293
- status?: number;
2294
- };
2295
- credits?: TraeWebCredits;
2296
- creditsError?: string;
2297
- /**
2298
- * Present only when the bound account is out of credit AND another account
2299
- * on this machine has some (issue #25). The card then offers a one-click
2300
- * switch instead of leaving a permanently empty panel. Absent in every
2301
- * other case so a healthy account never pays for the extra read.
2302
- */
2303
- alternatives?: readonly TraeWebCreditAlternative[];
2304
- /**
2305
- * Present only when the Trae app's CURRENT sign-in differs from the
2306
- * account this plugin is bound to (issue #29): the card then offers a
2307
- * one-click follow instead of making the user hunt the account dropdown
2308
- * after every app-side sign-out. Absent in every other case — a healthy
2309
- * document costs no extra read beyond the accounts scan the document
2310
- * already runs.
2311
- */
2312
- appLogin?: TraeWebAppLogin;
2313
3142
  /**
2314
- * Daily check-in state (CN only). Absent on the international region,
2315
- * whose check-in surface does not exist — probed 2026-09-24: the
2316
- * `/trae/api/v2/ug/*` family answers 404 on every ai gateway, so the card
2317
- * shows no claim button there rather than a button that cannot work.
3143
+ * - `claimed` — the upstream accepted today's claim for this account.
3144
+ * - `already` — this ACCOUNT's reward for today already exists, so nothing was sent.
3145
+ * - `device-already` — the MACHINE already spent today (9095). Not an account
3146
+ * failure and not a success: a truthful "no, this device is used up".
3147
+ * - `disabled` — the upstream's check-in activity is not enabled/active for
3148
+ * this account. Reported separately rather than as `failed`, because claiming
3149
+ * is impossible by policy and calling it an error would read as a broken
3150
+ * plugin.
3151
+ * - `failed` — the request never landed, or the credential could not be read.
2318
3152
  */
2319
- checkin?: TraeWebCheckin;
2320
- checkinError?: string;
2321
- /** Subscription status of an international account (region ai only). */
2322
- payStatus?: TraeWebPayStatus;
2323
- payStatusError?: string;
2324
- } | {
2325
- status: 'error';
2326
- message: string;
2327
- };
3153
+ status: 'claimed' | 'already' | 'device-already' | 'disabled' | 'failed';
3154
+ /** Credits the upstream says this claim carries, when it stated a figure. */
3155
+ credit?: number;
3156
+ /** The upstream's own words on failure/refusal, redacted by the caller. */
3157
+ message?: string;
3158
+ }
3159
+ /** One account's test outcome. */
3160
+ interface TraePoolTestRow {
3161
+ accountId: string;
3162
+ accountName: string;
3163
+ /** The model this row was probed on — one model per batch, recorded per row. */
3164
+ modelId: string;
3165
+ outcome: TraeProbeResult['verdict'];
3166
+ /** Upstream's own words / the plugin's reading of the code. */
3167
+ detail: string;
3168
+ /** A time the upstream named, when it named one. */
3169
+ retryAtMs?: number;
3170
+ }
2328
3171
  //#endregion
2329
3172
  //#region src/web-status.d.ts
2330
3173
  /**
2331
- * Constructor dependencies. Everything region-specific is addressed by the
2332
- * region the request names: the two regions are separate provider stacks, so
3174
+ * Constructor dependencies. Everything panel-specific is addressed by the
3175
+ * panel the request names: the two regions are separate provider stacks, so
2333
3176
  * the card must be served the directory, selection, and credits of the tab the
2334
3177
  * user is on.
2335
3178
  */
2336
3179
  interface TraeUsageRouteOptions {
2337
- /** The region-scoped credential store backing each region's requests. */
2338
- store(region: TraeRegion): TraeCredentialStore;
2339
- /** The region-scoped usage client (CN work credits / international pay status). */
2340
- client(region: TraeRegion): TraeUsageClient;
3180
+ /** The panel-scoped credential store backing each region's requests. */
3181
+ store(panel: TraePanel): TraeCredentialStore;
3182
+ /** The panel-scoped usage client (CN work credits / international pay status). */
3183
+ client(panel: TraePanel): TraeUsageClient;
2341
3184
  /**
2342
3185
  * CN credit balances of an account OTHER than the bound one, read without
2343
3186
  * changing which account is bound (issue #25). Optional: when absent the
2344
3187
  * exhausted-account hint is simply not offered.
2345
3188
  */
2346
- creditsOfAccount?(region: TraeRegion, accountId: string): Promise<TraeWebCredits | undefined>;
3189
+ creditsOfAccount?(panel: TraePanel, accountId: string): Promise<TraeWebCredits | undefined>;
2347
3190
  /**
2348
3191
  * Per-account status for one row of the card's account table: CN → check-in
2349
3192
  * + balances, ai → subscription. Read without changing which account is
2350
3193
  * bound. Optional: when absent the account rows show no check-in/credits/
2351
3194
  * subscription figures (the table still lists each account and its switch).
2352
3195
  */
2353
- accountStateOf?(region: TraeRegion, accountId: string): Promise<TraeWebAccountState | undefined>;
3196
+ accountStateOf?(panel: TraePanel, accountId: string): Promise<TraeWebAccountState | undefined>;
2354
3197
  /**
2355
3198
  * Claim today's check-in reward for ONE named account, without moving the
2356
3199
  * binding. Optional: absent means the per-row claim button is disabled (the
2357
3200
  * check-in surface simply is not offered).
2358
3201
  */
2359
- claimCheckinOf?(region: TraeRegion, accountId: string): Promise<{
3202
+ claimCheckinOf?(panel: TraePanel, accountId: string): Promise<{
2360
3203
  claim: TraeCheckinClaim;
2361
3204
  checkin: TraeCheckinStatus;
2362
3205
  } | undefined>;
@@ -2367,24 +3210,24 @@ interface TraeUsageRouteOptions {
2367
3210
  * Read-only — the HOST never rebinds from this value; the follow is a
2368
3211
  * settings write, made by the card or by the host's follow switch.
2369
3212
  */
2370
- liveLogin?(region: TraeRegion): Promise<TraeWebAppLogin | undefined>;
3213
+ liveLogin?(panel: TraePanel): Promise<TraeWebAppLogin | undefined>;
2371
3214
  /** The requested region's last-refreshed raw directory (one entry per upstream model). */
2372
- displayModels(region: TraeRegion): readonly TraeModelInfo[];
2373
- /** The user's model selection in the requested region, stored as model id (= Trae name). */
2374
- enabledModelIds(region: TraeRegion): readonly string[];
3215
+ displayModels(panel: TraePanel): readonly TraeModelInfo[];
3216
+ /** The user's model selection in the requested panel, stored as model id (= Trae name). */
3217
+ enabledModelIds(panel: TraePanel): readonly string[];
2375
3218
  /**
2376
3219
  * Whether the requested region's provider is currently offered to DSH. The
2377
3220
  * card renders this as the tab's on/off checkbox, so the switch reflects the
2378
3221
  * committed settings value rather than a local guess.
2379
3222
  */
2380
- regionEnabled(region: TraeRegion): boolean;
3223
+ regionEnabled(panel: TraePanel): boolean;
2381
3224
  /** Re-read one region's live directory from the upstream. */
2382
- discoverModels?(region: TraeRegion, signal?: AbortSignal): Promise<readonly TraeModelInfo[]>;
3225
+ discoverModels?(panel: TraePanel, signal?: AbortSignal): Promise<readonly TraeModelInfo[]>;
2383
3226
  /**
2384
3227
  * Test the given model ids for availability, one real (minimal) chat call
2385
3228
  * each. Present only when the region's chat path is wired.
2386
3229
  */
2387
- probeModels?(region: TraeRegion, ids: readonly string[]): Promise<{
3230
+ probeModels?(panel: TraePanel, ids: readonly string[]): Promise<{
2388
3231
  results: {
2389
3232
  id: string;
2390
3233
  verdict: string;
@@ -2392,78 +3235,87 @@ interface TraeUsageRouteOptions {
2392
3235
  }[];
2393
3236
  skipped?: string[];
2394
3237
  }>;
2395
- /** Raw-Chat capability state of the requested region. */
2396
- rawDiagnostic?(region: TraeRegion): TraeRawDiagnostic;
3238
+ /** Raw-Chat capability state of the requested panel. */
3239
+ rawDiagnostic?(panel: TraePanel): TraeRawDiagnostic;
3240
+ /**
3241
+ * The account pool's host side, when the panel can run one. Optional on
3242
+ * purpose: the pool is an add-on to the status document, and a harness without
3243
+ * it (a test host, or a panel stack that failed to build) must still get a
3244
+ * complete usage answer with no `pool` field.
3245
+ */
3246
+ pool?: TraePoolDeps;
3247
+ }
3248
+ /**
3249
+ * What the pool surface needs from the Host, addressed by panel.
3250
+ *
3251
+ * The split is deliberate. The card never learns a credential and never writes a
3252
+ * measurement: it sends an ACTION (`checkin` / `test` / `remove`) and reads the
3253
+ * ranking back out of the status document. Everything here is therefore either a
3254
+ * cheap local read (preferences, roster, saved names) or an explicitly user-
3255
+ * triggered spend (the batches). The one thing that must never happen on a GET is
3256
+ * an upstream call made for every row on every poll — the card already lazy-loads
3257
+ * per-account state, so `checkedInToday` is cached and skippable by design.
3258
+ */
3259
+ interface TraePoolDeps {
3260
+ /** The panel's saved pool preferences, read from live settings. */
3261
+ preferences(panel: TraePanel): TraePoolPreferences;
3262
+ /**
3263
+ * The full pool membership WITH live credit reads — the expensive version.
3264
+ * Only the status document's own assembly and an explicit batch call it.
3265
+ */
3266
+ members(panel: TraePanel): Promise<readonly TraePoolMember[]>;
3267
+ /**
3268
+ * The checked ids that still resolve to a local sign-in, from a local scan only.
3269
+ * The batch route's empty-pool guard runs this on every request, so it must not
3270
+ * cost an upstream call — {@link members} is not suitable there.
3271
+ */
3272
+ effectiveMemberAccountIds?(panel: TraePanel): Promise<readonly string[]>;
3273
+ /** Last measured display name per saved id, for members whose sign-in is gone. */
3274
+ memberNames?(panel: TraePanel): Promise<Record<string, string>>;
3275
+ /** Local sign-ins the pool does NOT cover, so the card can offer them to check. */
3276
+ otherAccounts?(panel: TraePanel): Promise<readonly TraePoolAccount[]>;
3277
+ /** Which account the request path would bill right now, after applying the pool. */
3278
+ currentAccountId?(panel: TraePanel): Promise<string | undefined>;
3279
+ /** Whether this panel's upstream rewards a daily check-in at all. */
3280
+ checkinSupported?(panel: TraePanel): boolean;
3281
+ /**
3282
+ * Today's check-in state per account, from the cached per-account reads.
3283
+ *
3284
+ * A missing entry means "not read", which the card renders as unknown rather
3285
+ * than as a definite "not checked in". `deviceCheckedIn` is the Trae half of the
3286
+ * pair: the daily claim is keyed to `x-device-id`, so an account can be unpaid
3287
+ * TODAY because this machine already spent its claim on another account.
3288
+ */
3289
+ checkinState?(panel: TraePanel): Promise<Record<string, TraePoolCheckinState>>;
3290
+ /** Run the check-in batch. Persists nothing to settings; drops its own caches. */
3291
+ checkin?(panel: TraePanel): Promise<readonly TraePoolCheckinRow[]>;
3292
+ /** Run the model-availability batch and persist its measurements. */
3293
+ test?(panel: TraePanel, modelId: string, onRow?: (row: TraePoolTestRow) => void): Promise<readonly TraePoolTestRow[]>;
3294
+ /** Probe ONE account against the target model (the row's own test button). */
3295
+ testOne?(panel: TraePanel, accountId: string): Promise<TraePoolTestRow | undefined>;
3296
+ /** Forget an account's measurement — used when the user removes it from the pool. */
3297
+ removeOne?(panel: TraePanel, accountId: string): Promise<boolean>;
3298
+ /** The panel's catalog, the source of the free-model default and the stale check. */
3299
+ catalog?(panel: TraePanel): readonly TraeModelInfo[];
3300
+ }
3301
+ /** One row's cached check-in facts; see {@link TraePoolDeps.checkinState}. */
3302
+ interface TraePoolCheckinState {
3303
+ checkedIn: boolean;
3304
+ deviceCheckedIn?: boolean;
3305
+ /** Whether this account is offered the reward at all; false renders `disabled`. */
3306
+ enabled?: boolean;
2397
3307
  }
2398
3308
  /**
2399
- * Assemble one region's card document. `region` is the tab the card is on; the
2400
- * region-scoped store already answers with only that region's accounts, so the
3309
+ * Assemble one region's card document. `panel` is the tab the card is on; the
3310
+ * panel-scoped store already answers with only that region's accounts, so the
2401
3311
  * document's model slots and account list are that region's by construction.
2402
3312
  * Sign-in state is read-only; credit is a live billing answer whose failure
2403
3313
  * degrades to `creditsError` rather than failing the whole document.
2404
3314
  */
2405
- declare function traeWebUsage(deps: TraeUsageRouteOptions, region: TraeRegion): Promise<TraeWebUsage>;
3315
+ declare function traeWebUsage(deps: TraeUsageRouteOptions, panel: TraePanel): Promise<TraeWebUsage>;
2406
3316
  /** Mount the GET usage route on an optional webServer context. */
2407
3317
  declare function registerTraeUsageRoute(ctx: Context$1, deps: TraeUsageRouteOptions): void;
2408
3318
  //#endregion
2409
- //#region src/model-probe.d.ts
2410
- /** How one model answered. */
2411
- type TraeProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
2412
- interface TraeProbeResult {
2413
- /** The model id as the card knows it. */
2414
- id: string;
2415
- verdict: TraeProbeVerdict;
2416
- /** Upstream's own words (already translated), or a short note. */
2417
- detail: string;
2418
- }
2419
- /**
2420
- * How long one probe may take. Trae answers a one-word prompt in a few seconds;
2421
- * beyond this the answer is not worth waiting for and is reported as unknown.
2422
- */
2423
- declare const TRAE_PROBE_TIMEOUT_MS = 30000;
2424
- /**
2425
- * Classify a failure that arrived as a thrown stream error rather than a
2426
- * returned result.
2427
- *
2428
- * The bridge errors the stream (instead of returning a result) when a refusal
2429
- * interrupts an answer that had already started; its message is the same
2430
- * translated text, so a named code is still decisive. Anything without a
2431
- * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
2432
- */
2433
- declare function classifyProbeError(message: string): {
2434
- verdict: TraeProbeVerdict;
2435
- detail: string;
2436
- };
2437
- /**
2438
- * Classify a completed upstream call.
2439
- *
2440
- * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
2441
- * arrived inside a 200 body has already been turned into a real error result
2442
- * (and its code translated into `message`) by the bridge — this function only
2443
- * has to read it back.
2444
- */
2445
- declare function classifyProbeResult(result: TraeChatResult, outputSeen: boolean): {
2446
- verdict: TraeProbeVerdict;
2447
- detail: string;
2448
- };
2449
- /** The minimal chat call one probe makes. */
2450
- declare function probeBody(model: string): string;
2451
- /**
2452
- * Probe one model through the bridge the plugin actually chats with, so the
2453
- * recorded verdict describes the user's real path (same wire resolution, same
2454
- * function stamping, same error translation).
2455
- */
2456
- declare function probeOneModel(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, id: string, timeoutMs?: number): Promise<TraeProbeResult>;
2457
- /**
2458
- * Probe a list of models ONE AT A TIME.
2459
- *
2460
- * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
2461
- * would manufacture the very `rate_limited`/`unknown` verdicts this feature
2462
- * must avoid. An authentication failure stops the run — every later probe would
2463
- * fail the same way, and spending calls to learn that again is waste.
2464
- */
2465
- declare function probeModelsSequentially(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, ids: readonly string[], onResult?: (result: TraeProbeResult, index: number) => void): Promise<TraeProbeResult[]>;
2466
- //#endregion
2467
3319
  //#region src/index.d.ts
2468
3320
  declare const name = "dsh-connect-trae";
2469
3321
  declare const inject: string[];
@@ -2535,6 +3387,15 @@ interface TraeRegionState {
2535
3387
  * beside dsh-connect-workbuddy's rows — so the setting applies here too.
2536
3388
  */
2537
3389
  creditPosition?: TraeCreditPosition;
3390
+ /**
3391
+ * This region's account pool (issue #44), as the card saves it.
3392
+ *
3393
+ * Read through {@link regionStateOf} and written by the card's pool section.
3394
+ * `enabled` absent means OFF: an account pool decides where money is spent, so
3395
+ * it is opt-in and a config written before it existed must keep billing the
3396
+ * account the user picked.
3397
+ */
3398
+ pool?: TraePoolPreferences;
2538
3399
  }
2539
3400
  interface Config {
2540
3401
  authFile?: string;
@@ -2550,7 +3411,7 @@ interface Config {
2550
3411
  * Per-region account selections, keyed `cn` | `ai`. Each region's tab writes
2551
3412
  * its own slot; tokens remain outside settings.
2552
3413
  */
2553
- accounts?: Partial<Record<TraeRegion, string>>;
3414
+ accounts?: Partial<Record<TraePanel, string>>;
2554
3415
  /**
2555
3416
  * Show the credit readout in the composer tool row: `Trae CN · <n>`, rendered
2556
3417
  * ONLY while the session's selected model belongs to this plugin, clickable
@@ -2593,7 +3454,7 @@ interface Config {
2593
3454
  * expose different rosters, so each keeps its own directory and selection
2594
3455
  * and switching accounts never drops the other region's picks.
2595
3456
  */
2596
- regions?: Partial<Record<TraeRegion, TraeRegionState>>;
3457
+ regions?: Partial<Record<TraePanel, TraeRegionState>>;
2597
3458
  /**
2598
3459
  * @deprecated Legacy single-slot fields from before the region split. They
2599
3460
  * predate international support and are read as the CN region's state when
@@ -2610,24 +3471,36 @@ interface Config {
2610
3471
  models?: TraeModelInfo[];
2611
3472
  }
2612
3473
  /**
2613
- * One region's saved state. A config written before the region split has only
3474
+ * One panel's saved state. A config written before the region split has only
2614
3475
  * the flat fields: those were always captured from the CN endpoint (the
2615
3476
  * plugin had no international support), so they are read as the CN state and
2616
3477
  * only when no explicit CN slot exists. The ai region never inherits them —
2617
3478
  * that inheritance is exactly the bug where a stale CN directory would be
2618
3479
  * intersected with the international catalog and silently drop the user's
2619
3480
  * picks (the same failure workbuddy fixed with its region split).
3481
+ *
3482
+ * Keyed by PANEL (`cn` | `enterprise` | `ai`), because each panel carries its
3483
+ * own switch, account binding and model selection. `enterprise` has no legacy
3484
+ * equivalent: it starts from an EMPTY state (its own directory, its own model
3485
+ * picks) and never inherits CN's, so switching it on cannot silently activate
3486
+ * a model list the user never chose for the tenant pool.
2620
3487
  */
2621
- declare function regionStateOf(config: Config, region: TraeRegion): TraeRegionState;
3488
+ declare function regionStateOf(config: Config, panel: TraePanel): TraeRegionState;
2622
3489
  /**
2623
- * Whether one region's provider is switched on (issue #11). Opt-out semantics:
3490
+ * Whether one panel's provider is switched on (issue #11). Opt-out semantics:
2624
3491
  * only an explicit `false` disables it, so every config written before this
2625
3492
  * switch existed — including the pre-region-split flat fields, which never
2626
3493
  * carry `enabled` — keeps both providers running exactly as before. The card
2627
3494
  * reads the same rule through `regionEnabledOf`, so the two halves can never
2628
3495
  * disagree about a region's state.
3496
+ *
3497
+ * The ENTERPRISE panel is the one deliberate exception: it defaults OFF. A
3498
+ * consumer-only account (`tenant 7o2d894p7dr0o4`) cannot use that pool at all,
3499
+ * so opting every existing user into a second CN provider would put a route in
3500
+ * their model picker that fails on first use. It is switched on from its card
3501
+ * tab, by someone whose account actually belongs to a tenant.
2629
3502
  */
2630
- declare function regionEnabled(config: Config, region: TraeRegion): boolean;
3503
+ declare function regionEnabled(config: Config, panel: TraePanel): boolean;
2631
3504
  /**
2632
3505
  * WHERE one region's credit readout sits (learned from workbuddy #62).
2633
3506
  *
@@ -2658,4 +3531,4 @@ declare function asVolatile<S>(schema: S): S;
2658
3531
  declare const Config: z<Config>;
2659
3532
  declare function apply(ctx: Context$1, config: Config): void;
2660
3533
  //#endregion
2661
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
3534
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, 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, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };