dsh-xdpool 2.4.2 → 2.4.4

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
@@ -934,6 +934,15 @@ export declare function candidateAuthDirs(env?: NodeJS.ProcessEnv): string[];
934
934
  /** How the pool chooses which account serves the next request. */
935
935
  type AccountDistribution = 'priority' | 'round-robin' | 'balanced' | 'sticky' | 'expiry';
936
936
  interface AccountPoolOptions {
937
+ /**
938
+ * File the `sticky` bindings are remembered in, or undefined for memory only.
939
+ *
940
+ * The card promises one account per conversation, and that promise used to end
941
+ * at the process boundary: every restart re-picked, so one session could be
942
+ * served by three different accounts in a day and pay full prompt cost each
943
+ * time. The caller passes the plugin's own state path; tests pass a temp file.
944
+ */
945
+ affinityPath?: string;
937
946
  /** Logger for discovery and rotation events. */
938
947
  logger?: {
939
948
  info?(...args: unknown[]): void;
@@ -1026,6 +1035,9 @@ export declare class WorkBuddyAccountPool {
1026
1035
  * the LRU order: re-binding deletes then re-inserts.
1027
1036
  */
1028
1037
  private readonly affinity;
1038
+ /** Where the bindings above are persisted; undefined keeps them in memory. */
1039
+ private readonly affinityPath;
1040
+ private affinitySaveTimer;
1029
1041
  private lastScanAtMs;
1030
1042
  /**
1031
1043
  * The account id the user pinned, or undefined for none.
@@ -1205,6 +1217,16 @@ export declare class WorkBuddyAccountPool {
1205
1217
  private pickRoundRobin;
1206
1218
  /** Remember which account a conversation is bound to, keeping LRU order. */
1207
1219
  private bindAffinity;
1220
+ /**
1221
+ * Restore bindings written by a previous process.
1222
+ *
1223
+ * Tolerant like the other state files: a malformed document or one bad entry
1224
+ * must not take the pool down, and an account id that no longer exists is
1225
+ * simply never matched (the next acquire re-picks and rebinds).
1226
+ */
1227
+ private loadAffinity;
1228
+ /** Write the bindings back, coalescing a burst into one atomic rename. */
1229
+ private scheduleAffinitySave;
1208
1230
  /**
1209
1231
  * `sticky`: the account this conversation already used, when it can still
1210
1232
  * serve the model being asked for.
@@ -1763,6 +1785,69 @@ interface PoolWebRelayPatch {
1763
1785
  host?: string;
1764
1786
  port?: number;
1765
1787
  key?: string;
1788
+ /**
1789
+ * The models to share with LAN clients, by gateway id.
1790
+ *
1791
+ * An empty array is an answer, not an absence: it means the user unticked
1792
+ * everything, and the gateway then refuses every model. Only "never set"
1793
+ * leaves the gateway open.
1794
+ */
1795
+ models?: string[];
1796
+ }
1797
+ /**
1798
+ * What {@link POOL_RELAY_PATH} answers with.
1799
+ *
1800
+ * `listening` is separate from `enabled` because "you turned it on and it could
1801
+ * not bind" is a state the user has to be able to see. Collapsing the two would
1802
+ * tell them to switch on something that is already on.
1803
+ *
1804
+ * `key` is the RELAY key, in full, on purpose: the card has to show it so it can
1805
+ * be pasted into another device. It is the key the plugin minted for LAN
1806
+ * clients — the shim's own per-process secret is never returned by any route.
1807
+ *
1808
+ * The type lives here rather than beside the listener because the client card
1809
+ * renders it, and importing the listener's module would drag `node:http` into a
1810
+ * browser bundle.
1811
+ */
1812
+ interface PoolWebRelayStatus {
1813
+ enabled: boolean;
1814
+ listening: boolean;
1815
+ host: string;
1816
+ port: number;
1817
+ /** Where a client should point; '' until something is listening. */
1818
+ endpoint: string;
1819
+ key: string;
1820
+ /** Why nothing is bound; '' when there is nothing to explain. */
1821
+ error: string;
1822
+ /**
1823
+ * Every model the gateway could serve, with the name a human reads.
1824
+ *
1825
+ * Built from the shim's own roster, so the picker cannot offer a model the
1826
+ * gateway would then refuse. The name is here because the OpenAI `/v1/models`
1827
+ * schema has no such field: `qfmodel` is what a client sends, and
1828
+ * `Qwen3.8-Flash · 免费` is what the user knows it as, and only this card can
1829
+ * show both together.
1830
+ *
1831
+ * REQUIRED, and required on the POST answer too: the card treats a write
1832
+ * response as the new state, so a save that answered without the roster emptied
1833
+ * the picker until the next poll. Declaring it optional let the write path
1834
+ * return the bare listener status and typecheck clean.
1835
+ */
1836
+ candidates: readonly PoolWebGatewayModel[];
1837
+ /**
1838
+ * The shared list in force. `undefined` means the user has never chosen, which
1839
+ * the card must render as "everything is shared" rather than an empty picker.
1840
+ */
1841
+ shared?: readonly string[];
1842
+ }
1843
+ /** One row of the gateway's model picker. */
1844
+ interface PoolWebGatewayModel {
1845
+ /** What a client sends, and what the allowlist stores. */
1846
+ id: string;
1847
+ /** The display name from the lane that serves it. */
1848
+ name: string;
1849
+ /** The lane or channel it belongs to; what the picker groups under. */
1850
+ source: string;
1766
1851
  }
1767
1852
  /**
1768
1853
  * What a key probe answered.
@@ -2451,6 +2536,21 @@ interface RelaySetting {
2451
2536
  port?: number;
2452
2537
  /** URL-safe shared secret LAN clients authenticate with. */
2453
2538
  key?: string;
2539
+ /**
2540
+ * Which models the gateway may serve, by gateway id (`buddy:hy3`,
2541
+ * `qoder:qfmodel`, `free:…`, `provider/model`).
2542
+ *
2543
+ * Absent and empty are DIFFERENT answers and the distinction is load-bearing:
2544
+ * absent means "no selection has ever been made, serve everything", so
2545
+ * upgrading does not silently break a gateway a device is already pointed at;
2546
+ * empty means "the user unticked everything", and is honoured as such.
2547
+ *
2548
+ * This is an allowlist enforced on the request, not just on the listing. A
2549
+ * relay key is one value shared by every device on the LAN, so "hidden from
2550
+ * /v1/models" would leave every other lane reachable to anything that guessed
2551
+ * an id — including the login channels, whose quota is this account's.
2552
+ */
2553
+ models?: string[];
2454
2554
  }
2455
2555
  //#endregion
2456
2556
  //#region src/aggregate.d.ts
@@ -2717,6 +2817,40 @@ declare class UsageRecorder {
2717
2817
  private days;
2718
2818
  }
2719
2819
  //#endregion
2820
+ //#region src/channel-lane.d.ts
2821
+ /** One model as the gateway lists and routes it. */
2822
+ interface ChannelLaneRow {
2823
+ /** The composite id a client sends: `<channel>:<model>`. */
2824
+ id: string;
2825
+ channel: string;
2826
+ modelId: string;
2827
+ name: string;
2828
+ }
2829
+ /** One translated event, so the caller decides how to frame it as SSE. */
2830
+ type ChannelLaneEvent = {
2831
+ kind: 'delta';
2832
+ delta: Record<string, unknown>;
2833
+ } | {
2834
+ kind: 'usage';
2835
+ usage: Record<string, number>;
2836
+ } | {
2837
+ kind: 'done';
2838
+ finishReason: string;
2839
+ };
2840
+ interface ChannelLane {
2841
+ rows: () => Promise<readonly ChannelLaneRow[]>;
2842
+ resolve: (id: string) => {
2843
+ channel: string;
2844
+ modelId: string;
2845
+ } | undefined;
2846
+ turn: (input: {
2847
+ channel: string;
2848
+ modelId: string;
2849
+ body: Record<string, unknown>;
2850
+ signal: AbortSignal;
2851
+ }) => AsyncGenerator<ChannelLaneEvent>;
2852
+ }
2853
+ //#endregion
2720
2854
  //#region src/shim.d.ts
2721
2855
  interface ShimLogger {
2722
2856
  info?(...args: unknown[]): void;
@@ -2727,8 +2861,29 @@ interface WorkBuddyShim {
2727
2861
  ready: Promise<void>;
2728
2862
  baseUrl(): string;
2729
2863
  token(): string;
2864
+ /**
2865
+ * Everything the gateway could serve, with display names.
2866
+ *
2867
+ * The card's model picker reads this; `/v1/models` maps the same entries down
2868
+ * to ids. One source so the card cannot offer a model the gateway then refuses.
2869
+ */
2870
+ roster(): Promise<readonly GatewayModelEntry[]>;
2730
2871
  close(): Promise<void>;
2731
2872
  }
2873
+ /**
2874
+ * One gateway-visible model: the id a client sends, the name a human reads, and
2875
+ * the lane it belongs to.
2876
+ *
2877
+ * Both are needed because they disagree in exactly the places that matter. A
2878
+ * qoder model is `qfmodel` on the wire and `Qwen3.8-Flash · 免费` in every
2879
+ * interface the user has met it in, and the OpenAI `/v1/models` schema has no
2880
+ * field for the second — so the name can only ever be shown here, on the card.
2881
+ */
2882
+ interface GatewayModelEntry {
2883
+ id: string;
2884
+ name: string;
2885
+ source: string;
2886
+ }
2732
2887
  /**
2733
2888
  * Everything the aggregate lane needs to answer one request.
2734
2889
  *
@@ -2752,9 +2907,18 @@ interface ExtraLaneOptions {
2752
2907
  free: ExtraLaneSource;
2753
2908
  }
2754
2909
  interface ExtraLaneSource {
2910
+ /**
2911
+ * The rows this lane offers.
2912
+ *
2913
+ * `name` is the human-facing display name the free catalog already keeps (a
2914
+ * raw upstream id is deliberately never shown to a picker); it is optional
2915
+ * here because the lane's own contract only ever needed the ids, and the
2916
+ * gateway's model picker is the first consumer that wants the label too.
2917
+ */
2755
2918
  current: () => readonly {
2756
2919
  modelId: string;
2757
2920
  id: string;
2921
+ name?: string;
2758
2922
  }[];
2759
2923
  /**
2760
2924
  * Run one turn, handing back the frames to write.
@@ -2809,6 +2973,29 @@ interface WorkBuddyShimOptions {
2809
2973
  * give the user two names for one lane.
2810
2974
  */
2811
2975
  extra?: ExtraLaneOptions;
2976
+ /**
2977
+ * The fifth lane: the 扫码登录 channels, reached through the host's own LLM
2978
+ * seam rather than a socket this plugin owns.
2979
+ *
2980
+ * CN-shim-only for the aggregate lane's reason — a second copy under the
2981
+ * global provider id would list every channel twice — and the relay is wired to
2982
+ * this shim, so this is also the only shim the LAN gateway can see.
2983
+ */
2984
+ channels?: ChannelLane;
2985
+ /**
2986
+ * The gateway allowlist, re-read per request.
2987
+ *
2988
+ * `undefined` means "no selection has been made: serve everything to LAN
2989
+ * clients too", so upgrading cannot silently break a device already pointed at
2990
+ * the relay. An empty array is the opposite answer — the user unticked
2991
+ * everything — and is enforced as such.
2992
+ *
2993
+ * Only consulted for requests carrying {@link RELAY_HOP_HEADER}, which is the
2994
+ * relay's own marker. The host's adapter reaches this same process without it,
2995
+ * so the models DSH offers in its picker are never narrowed by a list meant
2996
+ * for other machines.
2997
+ */
2998
+ lanModels?: () => readonly string[] | undefined;
2812
2999
  }
2813
3000
  export declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddyShim;
2814
3001
  //#endregion
@@ -3618,27 +3805,6 @@ export declare function readPreferredIdSync(path?: string): string | undefined;
3618
3805
  */
3619
3806
  export declare function writePreferredId(accountId: string | undefined, path?: string): Promise<void>;
3620
3807
  //#endregion
3621
- //#region src/relay.d.ts
3622
- interface PoolRelayStatus {
3623
- enabled: boolean;
3624
- /** Whether a listener is actually bound right now. */
3625
- listening: boolean;
3626
- host: string;
3627
- port: number;
3628
- /** Where a client should point; '' until something is listening. */
3629
- endpoint: string;
3630
- /**
3631
- * The relay key in full.
3632
- *
3633
- * Returned on purpose: the card has to show it once so the user can copy it
3634
- * into the other device. This is the key WE minted for LAN clients, not the
3635
- * shim secret, and it grants nothing beyond reaching the pool.
3636
- */
3637
- key: string;
3638
- /** Why nothing is bound; '' when there is nothing to explain. */
3639
- error: string;
3640
- }
3641
- //#endregion
3642
3808
  //#region src/web-status.d.ts
3643
3809
  /** Constructor dependencies — a narrow slice of the pool runtime. */
3644
3810
  interface PoolStatusRouteOptions {
@@ -3836,9 +4002,9 @@ interface PoolStatusRouteOptions {
3836
4002
  * the plugin minted for LAN clients. The shim's own per-process secret is never
3837
4003
  * exposed by any route.
3838
4004
  */
3839
- relayDocument?: () => Promise<PoolRelayStatus>;
4005
+ relayDocument?: () => Promise<PoolWebRelayStatus>;
3840
4006
  /** Apply a relay patch, re-evaluate the listener, and answer with the new state. */
3841
- saveRelay?: (patch: PoolWebRelayPatch) => Promise<PoolRelayStatus>;
4007
+ saveRelay?: (patch: PoolWebRelayPatch) => Promise<PoolWebRelayStatus>;
3842
4008
  }
3843
4009
  /**
3844
4010
  * Assemble the card's status document. Per-account credits and check-in state