dsh-plugin-local-infer 0.1.2 → 0.1.3

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.
Files changed (100) hide show
  1. package/README.md +10 -2
  2. package/docs/zh/README.md +10 -2
  3. package/dsh/client.js +669 -191
  4. package/package.json +6 -1
  5. package/packages/core/lib/adapter.d.ts +44 -2
  6. package/packages/core/lib/deps.d.ts +4 -3
  7. package/packages/core/lib/index.d.ts +1 -1
  8. package/packages/core/lib/registrar.d.ts +5 -5
  9. package/packages/core/lib/registrar.js +3 -3
  10. package/packages/core/package.json +1 -1
  11. package/packages/providers/lib/acquire.d.ts +5 -3
  12. package/packages/providers/lib/acquire.js +30 -9
  13. package/packages/providers/lib/adapters.d.ts +19 -209
  14. package/packages/providers/lib/adapters.js +13 -886
  15. package/packages/providers/lib/deps.d.ts +24 -2
  16. package/packages/providers/lib/deps.js +23 -4
  17. package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
  18. package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
  19. package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
  20. package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
  21. package/packages/providers/lib/engines/infernixAdapter.js +202 -0
  22. package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
  23. package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
  24. package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
  25. package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
  26. package/packages/providers/lib/engines/ninferAdapter.js +324 -0
  27. package/packages/providers/lib/engines/strata.d.ts +53 -0
  28. package/packages/providers/lib/engines/strata.js +197 -0
  29. package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
  30. package/packages/providers/lib/engines/strataAdapter.js +427 -0
  31. package/packages/providers/lib/engines.d.ts +5 -4
  32. package/packages/providers/lib/engines.js +10 -10
  33. package/packages/providers/lib/guidelines.js +1 -1
  34. package/packages/providers/lib/index.d.ts +14 -9
  35. package/packages/providers/lib/index.js +17 -7
  36. package/packages/providers/lib/model-defaults.d.ts +5 -4
  37. package/packages/providers/lib/model-defaults.js +22 -4
  38. package/packages/providers/lib/platform.js +7 -1
  39. package/packages/providers/package.json +1 -1
  40. package/packages/service/lib/index.d.ts +13 -4
  41. package/packages/service/lib/index.js +79 -5
  42. package/packages/service/lib/types.d.ts +5 -4
  43. package/packages/service/package.json +1 -1
  44. package/packages/tool/package.json +1 -1
  45. package/packages/ui/lib/config.d.ts +79 -0
  46. package/packages/ui/lib/config.js +235 -0
  47. package/packages/ui/lib/deps.d.ts +3 -3
  48. package/packages/ui/lib/ops/engine.d.ts +108 -0
  49. package/packages/ui/lib/ops/engine.js +493 -0
  50. package/packages/ui/lib/ops/hf.d.ts +27 -0
  51. package/packages/ui/lib/ops/hf.js +60 -0
  52. package/packages/ui/lib/ops/logs.d.ts +23 -0
  53. package/packages/ui/lib/ops/logs.js +58 -0
  54. package/packages/ui/lib/ops/model.d.ts +79 -0
  55. package/packages/ui/lib/ops/model.js +130 -0
  56. package/packages/ui/lib/request.d.ts +106 -0
  57. package/packages/ui/lib/request.js +80 -0
  58. package/packages/ui/lib/route.d.ts +88 -420
  59. package/packages/ui/lib/route.js +111 -1843
  60. package/packages/ui/lib/routes/chat-template.d.ts +14 -0
  61. package/packages/ui/lib/routes/chat-template.js +37 -0
  62. package/packages/ui/lib/routes/deps.d.ts +16 -0
  63. package/packages/ui/lib/routes/deps.js +27 -0
  64. package/packages/ui/lib/routes/download.d.ts +16 -0
  65. package/packages/ui/lib/routes/download.js +48 -0
  66. package/packages/ui/lib/routes/get.d.ts +18 -0
  67. package/packages/ui/lib/routes/get.js +57 -0
  68. package/packages/ui/lib/routes/install.d.ts +16 -0
  69. package/packages/ui/lib/routes/install.js +36 -0
  70. package/packages/ui/lib/routes/launch.d.ts +18 -0
  71. package/packages/ui/lib/routes/launch.js +50 -0
  72. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  73. package/packages/ui/lib/routes/provider-delete.js +37 -0
  74. package/packages/ui/lib/routes/register.d.ts +16 -0
  75. package/packages/ui/lib/routes/register.js +28 -0
  76. package/packages/ui/lib/routes/save.d.ts +18 -0
  77. package/packages/ui/lib/routes/save.js +139 -0
  78. package/packages/ui/lib/routes/stop.d.ts +16 -0
  79. package/packages/ui/lib/routes/stop.js +29 -0
  80. package/packages/ui/lib/routes/summary.d.ts +15 -0
  81. package/packages/ui/lib/routes/summary.js +26 -0
  82. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  83. package/packages/ui/lib/routes/unregister.js +26 -0
  84. package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
  85. package/packages/ui/lib/viewmodels/deps.js +89 -0
  86. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
  87. package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
  88. package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
  89. package/packages/ui/lib/viewmodels/summary.js +225 -0
  90. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  91. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  92. package/packages/ui/package.json +1 -1
  93. package/packages/providers/lib/strata.d.ts +0 -15
  94. package/packages/providers/lib/strata.js +0 -14
  95. /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
  96. /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
  97. /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
  98. /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
  99. /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
  100. /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
@@ -5,7 +5,9 @@
5
5
  * defaults are a data edit here, never a card rewire.
6
6
  * @module dsh-plugin-local-infer/providers/platform
7
7
  */
8
- import { LLAMACPP_DOCKER_IMAGE, NINFER_DOCKER_IMAGE, STRATA_DOCKER_IMAGE, } from "./adapters.js";
8
+ import { LLAMACPP_DOCKER_IMAGE } from "./engines/llamacppAdapter.js";
9
+ import { NINFER_DOCKER_IMAGE } from "./engines/ninferAdapter.js";
10
+ import { STRATA_DOCKER_IMAGE } from "./engines/strataAdapter.js";
9
11
  import { engineBuildSteps } from "./acquire.js";
10
12
  // The local build command is the engine's first acquisition build step (the
11
13
  // cmake build the deploy step runs in the checkout), read through
@@ -15,6 +17,7 @@ import { engineBuildSteps } from "./acquire.js";
15
17
  const NINFER_BUILD_COMMAND = engineBuildSteps('ninfer')[0];
16
18
  const LLAMACPP_BUILD_COMMAND = engineBuildSteps('llamacpp')[0];
17
19
  const INFERNIX_BUILD_COMMAND = engineBuildSteps('infernix')[0];
20
+ const STRATA_BUILD_COMMAND = engineBuildSteps('strata')[0];
18
21
  /**
19
22
  * The per-engine platform defaults, keyed by engine id. The build command is
20
23
  * the engine's first acquisition build step; the docker tag is the upstream
@@ -45,6 +48,9 @@ export const ENGINE_PLATFORM_DEFAULTS = {
45
48
  : {}),
46
49
  },
47
50
  strata: {
51
+ ...(STRATA_BUILD_COMMAND !== undefined
52
+ ? { defaultBuildCommand: STRATA_BUILD_COMMAND }
53
+ : {}),
48
54
  defaultDockerTag: STRATA_DOCKER_IMAGE,
49
55
  },
50
56
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-local-infer-providers",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Concrete providers for the dsh local-inference plugin: the ninfer, hyperqwen, llamacpp, and strata engine adapters, the NInfer VramModel, the built-in presets, the built-in guidelines, and the per-OS SystemDep install command data. Each registers itself with ctx.localInfer.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -18,7 +18,7 @@
18
18
  * @module dsh-plugin-local-infer/service
19
19
  */
20
20
  import { Context, Service } from '@deepseek-ai/cordis';
21
- import type { Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EnginePlatformConfig, GpuUsageProbe, Guideline, GuidelineResult, HardwareProfile, HardwareProbes, InstallResult, LaunchPlan, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ResolvedDefaults, SystemDep, VramModel } from 'dsh-plugin-local-infer/core';
21
+ import type { BuildMode, Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EnginePlatformConfig, GpuUsageProbe, Guideline, GuidelineResult, HardwareProfile, HardwareProbes, InstallResult, LaunchPlan, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ResolvedDefaults, SystemDep, VramModel } from 'dsh-plugin-local-infer/core';
22
22
  import type { DetectOsReserveOptions, DownloadModelOptions, DownloadResult, EngineDeathEvent, EstimateResult, FetchPresetOptions, FetchPresetResult, HfCliOptions, LaunchOptions, LaunchResult, LocalInferService, ProviderStatusOptions, RegisterModelOptions } from './types.ts';
23
23
  export type { DetectOsReserveOptions, DownloadModelOptions, DownloadResult, EngineDeathEvent, EstimateResult, FetchPresetOptions, FetchPresetResult, HfCliOptions, LaunchOptions, LaunchResult, LocalInferService, ProviderStatusOptions, RegisterModelOptions, SettingsDescribeResult, SettingsForms, SettingsNamespaceDescriptor, } from './types.ts';
24
24
  declare module '@deepseek-ai/cordis' {
@@ -53,6 +53,8 @@ export declare class LocalInfer extends Service implements LocalInferService {
53
53
  private readonly launchedPorts;
54
54
  private readonly engineWatchers;
55
55
  private readonly handledPids;
56
+ private readonly launchingEngines;
57
+ private readonly stoppingEngines;
56
58
  private deathObserver;
57
59
  constructor(ctx: Context);
58
60
  registerEngine(adapter: EngineAdapter): () => void;
@@ -168,19 +170,19 @@ export declare class LocalInfer extends Service implements LocalInferService {
168
170
  * registered plan for the engine id is the fallback, and an engine with
169
171
  * neither plans an empty list (nothing to install, the gate passes).
170
172
  */
171
- deps(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<readonly SystemDep[]>;
173
+ deps(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<readonly SystemDep[]>;
172
174
  /**
173
175
  * installString renders the copy/paste block for the resolved dep plan,
174
176
  * keyed to the detected package manager, with the by-hand note for a dep
175
177
  * the manager has no commands for.
176
178
  */
177
- installString(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<string>;
179
+ installString(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<string>;
178
180
  /**
179
181
  * runInstall is the Play/Run path: it runs the resolved dep plan's commands
180
182
  * through the shell runner and re-probes each dep. The runner captures the
181
183
  * per-command exit, so a failure names the command that broke.
182
184
  */
183
- runInstall(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<InstallResult>;
185
+ runInstall(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<InstallResult>;
184
186
  /**
185
187
  * launch is the lifecycle entry point: it resolves the adapter that rendered
186
188
  * the plan (by the plan's engineId), runs the adapter's deploy step (build
@@ -238,6 +240,13 @@ export declare class LocalInfer extends Service implements LocalInferService {
238
240
  * record dead.
239
241
  */
240
242
  private watchAdoptedEngine;
243
+ /**
244
+ * engineKey is the in-flight state machine's key for one engine: the engine
245
+ * id plus its port when the plan names one, so two configs of one engine on
246
+ * different ports are distinct engines. A plan with no port keys on the
247
+ * engine id alone.
248
+ */
249
+ private engineKey;
241
250
  /**
242
251
  * stopEngineWatcher cancels one port's adopted-process poll, if it has one.
243
252
  */
@@ -94,6 +94,20 @@ export class LocalInfer extends Service {
94
94
  // handle. A summary re-link that adopts this DSH's own launch record must
95
95
  // not stack a poll watcher on top of the handle's close event.
96
96
  handledPids = new Set();
97
+ // The engine keys (engineId:port) whose launch this service is in flight
98
+ // for: the deploy has started and the health poll has not settled. While a
99
+ // key is here the provider-status detector keeps the provider's linked read
100
+ // (running, not unsure), so an engine that sizes its own model (Strata, no
101
+ // model tie) or a launch mid-poll never flashes the "Unable to link" banner.
102
+ // The key clears when the launch settles (a healthy poll, a failed deploy).
103
+ launchingEngines = new Set();
104
+ // The engine keys (engineId:port) whose stop this service is in flight for:
105
+ // the stop step signalled the engine and the process has not died yet. While
106
+ // a key is here the detector keeps the linked read, so a stop never reads as
107
+ // an unlinked provider for the moment the engine takes to shut down. The key
108
+ // clears when the process dies (the death observer) or the detector sees the
109
+ // process down (a stop of an engine it cannot watch).
110
+ stoppingEngines = new Set();
97
111
  // The route-registered hook for engine process deaths (a crash, a pkill, or
98
112
  // a poll-detected death of an adopted process). The observer persists the
99
113
  // provider entry's cleared state; a headless profile registers none and the
@@ -352,7 +366,29 @@ export class LocalInfer extends Service {
352
366
  const port = signatures[index]?.port;
353
367
  return port !== undefined ? { ...provider, port } : provider;
354
368
  });
355
- return computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
369
+ const statuses = computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
370
+ // The in-flight launch and stop state machine: a provider the launch or
371
+ // stop step is in flight for keeps its linked read (running, not unsure)
372
+ // while the engine process is still up, so neither a stop (the record is
373
+ // cleared but the process is still shutting down) nor a launch (an engine
374
+ // that sizes its own model carries no model tie) flashes the "Unable to
375
+ // link" banner. A stopped provider whose process is down reads as stopped,
376
+ // and the stopping flag clears with it, so a stop of an engine the service
377
+ // cannot watch (no death event) never leaves a stale flag behind.
378
+ for (const provider of priced) {
379
+ const key = this.engineKey(provider.engine, provider.port);
380
+ const launching = this.launchingEngines.has(key);
381
+ const stopping = this.stoppingEngines.has(key);
382
+ if (!launching && !stopping)
383
+ continue;
384
+ if (statuses[provider.id] === 'unsure') {
385
+ statuses[provider.id] = 'running';
386
+ }
387
+ else if (stopping) {
388
+ this.stoppingEngines.delete(key);
389
+ }
390
+ }
391
+ return statuses;
356
392
  }
357
393
  /**
358
394
  * unsureBannerKillCommands returns the bash kill commands for the engine
@@ -472,8 +508,16 @@ export class LocalInfer extends Service {
472
508
  ...(options?.autoRebuild !== undefined ? { autoRebuild: options.autoRebuild } : {}),
473
509
  }
474
510
  : undefined;
511
+ // The in-flight launch flag: the provider-status detector keeps this
512
+ // provider's linked read while the deploy and health poll run, so an engine
513
+ // that sizes its own model (no model tie) never flashes "Unable to link".
514
+ const launchKey = this.engineKey(plan.engineId, plan.port);
515
+ this.launchingEngines.add(launchKey);
475
516
  const deployResult = await adapter.deploy(plan, platform, onProgress, deployOptions);
476
517
  if (!deployResult.ok) {
518
+ // A failed deploy settles the launch: clear the in-flight flag so the
519
+ // provider reads from the process state (stopped), not a stuck launch.
520
+ this.launchingEngines.delete(launchKey);
477
521
  // exactOptionalPropertyTypes: only write the detail key when it is present.
478
522
  const result = { plan, healthy: false };
479
523
  if (deployResult.detail !== undefined) {
@@ -517,10 +561,20 @@ export class LocalInfer extends Service {
517
561
  // "Launching..." while it waits for the engine to report ready.
518
562
  onProgress?.('launch');
519
563
  const health = await adapter.healthCheck(plan, options);
564
+ // The launch has settled (a healthy poll, a failed one, or a download that
565
+ // the card re-polls): clear the in-flight flag so the provider reads from
566
+ // the model tie (a healthy launch) or the process state (a failed one).
567
+ this.launchingEngines.delete(launchKey);
520
568
  const launchResult = { plan, healthy: health.healthy };
521
569
  if (health.detail !== undefined) {
522
570
  launchResult.detail = health.detail;
523
571
  }
572
+ // The start phase the poll observed (downloading, starting, ready, failed)
573
+ // rides the result so the card reads a long model download as in progress
574
+ // instead of a broken launch.
575
+ if (health.state !== undefined) {
576
+ launchResult.state = health.state;
577
+ }
524
578
  // The launch record the route persists for the provider config entry: the
525
579
  // engine's pid and the DSH process pid that launched it. The parent pid is
526
580
  // only written when the host process identifies as DSH, so a non-DSH host
@@ -671,6 +725,15 @@ export class LocalInfer extends Service {
671
725
  timer.unref();
672
726
  this.engineWatchers.set(port, { pid: record.pid, timer });
673
727
  }
728
+ /**
729
+ * engineKey is the in-flight state machine's key for one engine: the engine
730
+ * id plus its port when the plan names one, so two configs of one engine on
731
+ * different ports are distinct engines. A plan with no port keys on the
732
+ * engine id alone.
733
+ */
734
+ engineKey(engineId, port) {
735
+ return port !== undefined ? `${engineId}:${port}` : engineId;
736
+ }
674
737
  /**
675
738
  * stopEngineWatcher cancels one port's adopted-process poll, if it has one.
676
739
  */
@@ -693,6 +756,10 @@ export class LocalInfer extends Service {
693
756
  */
694
757
  observeEngineDeath(event) {
695
758
  if (event.port !== undefined) {
759
+ // The process is down: the in-flight stop (if one was in flight) has
760
+ // settled, so drop the flag and the deferred port-to-model tie the stop
761
+ // step kept for the provider's linked read.
762
+ this.stoppingEngines.delete(this.engineKey(event.engineId, event.port));
696
763
  const tracked = this.launchedModels.get(event.port);
697
764
  if (tracked !== undefined && tracked.engineId === event.engineId) {
698
765
  this.launchedModels.delete(event.port);
@@ -722,12 +789,19 @@ export class LocalInfer extends Service {
722
789
  if (adapter === undefined) {
723
790
  throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
724
791
  }
792
+ // The in-flight stop flag: the provider-status detector keeps this
793
+ // provider's linked read while the engine shuts down, so a stop never
794
+ // flashes the "Unable to link" banner for the moment the process takes to
795
+ // die. The flag clears when the process dies (the death observer) or the
796
+ // detector sees the process down (a stop of an engine it cannot watch).
797
+ this.stoppingEngines.add(this.engineKey(plan.engineId, plan.port));
725
798
  await adapter.stop(plan);
726
- // Clear the port-to-model entry too, so the provider-status detector no
727
- // longer ties this port to the model the plugin launched it with, and
728
- // cancel any adopted-process watcher standing on the port.
799
+ // Clear the live-port ledger and the plugin-launched flag, and cancel any
800
+ // adopted-process watcher standing on the port. The port-to-model tie is
801
+ // deferred to the death observer, so the provider keeps its linked read
802
+ // (running, not unsure) for the moment the engine is still shutting down;
803
+ // the observer drops it the instant the process is down.
729
804
  if (plan.port !== undefined) {
730
- this.launchedModels.delete(plan.port);
731
805
  this.stopEngineWatcher(plan.port);
732
806
  const ports = this.launchedPorts.get(plan.engineId);
733
807
  if (ports !== undefined) {
@@ -1,4 +1,4 @@
1
- import type { CommandOutcome, Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EnginePlatformConfig, Guideline, GuidelineResult, GpuUsageProbe, HardwareProfile, InstallResult, LaunchPlan, LaunchProgress, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ReserveSource, ResolvedDefaults, SettingsOp, SystemDep, VramBreakdown, VramModel, VramVerdict } from 'dsh-plugin-local-infer/core';
1
+ import type { BuildMode, CommandOutcome, Config, DefaultModel, ModelDirectoryEntry, EngineAdapter, EngineDepPlan, EngineLaunchState, EnginePlatformConfig, Guideline, GuidelineResult, GpuUsageProbe, HardwareProfile, InstallResult, LaunchPlan, LaunchProgress, ModelRow, Preset, PresetSource, ProcessLine, ProviderLaunchRecord, ProviderRef, ProviderStatus, ReLinkOutcome, RegisterResult, ReserveResolution, ReserveSource, ResolvedDefaults, SettingsOp, SystemDep, VramBreakdown, VramModel, VramVerdict } from 'dsh-plugin-local-infer/core';
2
2
  /**
3
3
  * EstimateResult is the answer to an estimate call: the full VRAM breakdown,
4
4
  * the one-word verdict the meter renders, the source the numbers came from,
@@ -79,6 +79,7 @@ export interface LaunchResult {
79
79
  buildLogs?: string;
80
80
  pid?: number;
81
81
  parentPid?: number;
82
+ state?: EngineLaunchState;
82
83
  }
83
84
  /**
84
85
  * LaunchOptions tunes the launch health poll: the interval between attempts
@@ -239,9 +240,9 @@ export interface LocalInferService {
239
240
  gpuUsage?: GpuUsageProbe | null;
240
241
  }): Promise<string[]>;
241
242
  guidelines(config: Config, hw: HardwareProfile, engineId?: string): Promise<GuidelineResult[]>;
242
- deps(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<readonly SystemDep[]>;
243
- installString(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<string>;
244
- runInstall(engineId: string, buildMode: 'docker' | 'local', hw: HardwareProfile): Promise<InstallResult>;
243
+ deps(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<readonly SystemDep[]>;
244
+ installString(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<string>;
245
+ runInstall(engineId: string, buildMode: BuildMode, hw: HardwareProfile): Promise<InstallResult>;
245
246
  launch(plan: LaunchPlan, options?: LaunchOptions): Promise<LaunchResult>;
246
247
  stop(plan: LaunchPlan): Promise<void>;
247
248
  reLinkProviders(entries: readonly {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-local-infer-service",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Service Definition for the dsh local-inference capability seam (ctx.localInfer): the engine, preset, and guideline registries plus the orchestration entry points (estimate, guidelines, deps, installString, runInstall, launch, registerModel). Carries no store of its own.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-local-infer-tool",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Model-facing tools for the dsh local-inference plugin: local_infer_estimate, local_infer_launch, local_infer_status, and local_infer_register, each on its own timeout budget, plus the system-prompt section that tells the agent when to use them",
5
5
  "keywords": [
6
6
  "dsh",
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The stored-config persistence the routes and ops share. The values live in
3
+ * ~/.dsh/local-infer/config.json, shared with the CLI, so the browser never
4
+ * sees a secret it did not store. Every read sanitizes the file field by
5
+ * field, so a by-hand edit never poisons the deploy step or the re-link rule.
6
+ * @module dsh-plugin-local-infer/ui/config
7
+ */
8
+ import type { Config, ProviderLaunchRecord } from 'dsh-plugin-local-infer/core';
9
+ /**
10
+ * The config file the route owns. It sits under the dsh home so a web user and
11
+ * a CLI user share one file, the way modsearch shares ~/.modsearch.
12
+ */
13
+ export declare function localInferConfigPath(): string;
14
+ /**
15
+ * StoredPlatformConfig is the per-engine platform config the deploy step
16
+ * consumes (§3.10): where the engine platform lives locally and the git repo
17
+ * that overrides the engine's default upstream. The map is keyed by engine id,
18
+ * so the stored entry carries no id of its own; the route adds it when it
19
+ * builds the EnginePlatformConfig the service registry stores.
20
+ */
21
+ export interface StoredPlatformConfig {
22
+ engineDirectory?: string;
23
+ engineFork?: string;
24
+ buildCommands?: string;
25
+ dockerTag?: string;
26
+ }
27
+ /**
28
+ * StoredProviderConfig is one saved provider model+engine config the card
29
+ * lists: a stable id the card edits and deletes against, the engine and the
30
+ * full parameter config it serves, the per-engine platform config the deploy
31
+ * step consumes, and the launch record the launch step persists when it
32
+ * starts the entry's engine (the engine pid, start time, launch arguments,
33
+ * and the DSH process pid that launched it). The record is what a DSH
34
+ * restart re-links a surviving engine to its config; a stop clears it.
35
+ */
36
+ export interface StoredProviderConfig {
37
+ id: string;
38
+ engine: string;
39
+ config: Config;
40
+ platform: StoredPlatformConfig;
41
+ launch?: ProviderLaunchRecord;
42
+ }
43
+ export interface StoredConfig {
44
+ engine: string;
45
+ config: Config;
46
+ allowRunDeps: boolean;
47
+ platform: Record<string, StoredPlatformConfig>;
48
+ showHelp: boolean;
49
+ providers: StoredProviderConfig[];
50
+ logPath: string;
51
+ lastLogs: string;
52
+ }
53
+ /**
54
+ * readConfig reads the stored engine, config, trust gate, and per-engine
55
+ * platform configs. A missing or broken file reads as the scaffold default
56
+ * (NInfer, empty config, gate off, no platform configs) so the card always has
57
+ * a form to draw. A platform entry that is not an object (or whose fields are
58
+ * not strings) is dropped, so a by-hand edit never poisons the deploy step.
59
+ */
60
+ export declare function readConfig(): StoredConfig;
61
+ /**
62
+ * writeConfig persists the engine, config, trust gate, per-engine platform
63
+ * configs, and the help toggle, creating the directory tree on first write.
64
+ */
65
+ export declare function writeConfig(engine: string, config: Config, allowRunDeps: boolean, platform?: Record<string, StoredPlatformConfig>, showHelp?: boolean, providers?: StoredProviderConfig[], logPath?: string, lastLogs?: string): void;
66
+ /**
67
+ * parsePlatformConfigs reads the stored platform map, keeping only entries
68
+ * whose fields are strings. A non-object map or a non-object entry reads as
69
+ * absent, so the deploy step never sees a platform config it cannot price.
70
+ */
71
+ export declare function parsePlatformConfigs(raw: unknown): Record<string, StoredPlatformConfig>;
72
+ /**
73
+ * parseStoredPlatformConfig reads one engine's platform config (the launch
74
+ * request's `platform` key), keeping only string fields. A non-object or an
75
+ * object with no string fields reads as absent, so the launch keeps the stored
76
+ * entry the save route synced.
77
+ */
78
+ export declare function parseStoredPlatformConfig(raw: unknown): StoredPlatformConfig | undefined;
79
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The stored-config persistence the routes and ops share. The values live in
3
+ * ~/.dsh/local-infer/config.json, shared with the CLI, so the browser never
4
+ * sees a secret it did not store. Every read sanitizes the file field by
5
+ * field, so a by-hand edit never poisons the deploy step or the re-link rule.
6
+ * @module dsh-plugin-local-infer/ui/config
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ // Relative, not the root package subpath: a self-referencing bare specifier
12
+ // only resolves when the package sits inside a node_modules tree, so a
13
+ // local-path or pnpm-linked install of this repo would fail the import.
14
+ import { DEFAULT_LOG_PATH_TEMPLATE } from '../../core/lib/index.js';
15
+ /**
16
+ * The config file the route owns. It sits under the dsh home so a web user and
17
+ * a CLI user share one file, the way modsearch shares ~/.modsearch.
18
+ */
19
+ export function localInferConfigPath() {
20
+ return join(homedir(), '.dsh', 'local-infer', 'config.json');
21
+ }
22
+ /**
23
+ * readConfig reads the stored engine, config, trust gate, and per-engine
24
+ * platform configs. A missing or broken file reads as the scaffold default
25
+ * (NInfer, empty config, gate off, no platform configs) so the card always has
26
+ * a form to draw. A platform entry that is not an object (or whose fields are
27
+ * not strings) is dropped, so a by-hand edit never poisons the deploy step.
28
+ */
29
+ export function readConfig() {
30
+ const path = localInferConfigPath();
31
+ if (!existsSync(path)) {
32
+ return {
33
+ engine: 'ninfer',
34
+ config: {},
35
+ allowRunDeps: false,
36
+ platform: {},
37
+ showHelp: true,
38
+ providers: [],
39
+ logPath: DEFAULT_LOG_PATH_TEMPLATE,
40
+ lastLogs: '',
41
+ };
42
+ }
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
45
+ return {
46
+ engine: typeof parsed.engine === 'string' ? parsed.engine : 'ninfer',
47
+ config: parsed.config && typeof parsed.config === 'object' ? parsed.config : {},
48
+ allowRunDeps: typeof parsed.allowRunDeps === 'boolean' ? parsed.allowRunDeps : false,
49
+ platform: parsePlatformConfigs(parsed.platform),
50
+ // A file that predates the key (or carries a non-boolean) reads as
51
+ // descriptions visible, the default the card draws on first open.
52
+ showHelp: typeof parsed.showHelp === 'boolean' ? parsed.showHelp : true,
53
+ providers: parseProviders(parsed.providers),
54
+ // A file that predates the key reads as the plugin's default log dir,
55
+ // and the last-run logs start empty until a launch fills them.
56
+ logPath: typeof parsed.logPath === 'string' && parsed.logPath !== ''
57
+ ? parsed.logPath
58
+ : DEFAULT_LOG_PATH_TEMPLATE,
59
+ lastLogs: typeof parsed.lastLogs === 'string' ? parsed.lastLogs : '',
60
+ };
61
+ }
62
+ catch {
63
+ return {
64
+ engine: 'ninfer',
65
+ config: {},
66
+ allowRunDeps: false,
67
+ platform: {},
68
+ showHelp: true,
69
+ providers: [],
70
+ logPath: DEFAULT_LOG_PATH_TEMPLATE,
71
+ lastLogs: '',
72
+ };
73
+ }
74
+ }
75
+ /**
76
+ * parseProviders reads the stored provider list, keeping only entries with a
77
+ * non-blank string id. A non-object entry or an entry with no id is dropped,
78
+ * so a by-hand edit never poisons the card's provider list. The per-engine
79
+ * platform config is read through the same string-field rule the platform map
80
+ * uses, so a provider never carries a platform field it cannot price.
81
+ */
82
+ function parseProviders(raw) {
83
+ if (raw === null || typeof raw !== 'object' || !Array.isArray(raw)) {
84
+ return [];
85
+ }
86
+ const providers = [];
87
+ for (const entry of raw) {
88
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
89
+ continue;
90
+ const fields = entry;
91
+ if (typeof fields.id !== 'string' || fields.id.trim() === '')
92
+ continue;
93
+ const engine = typeof fields.engine === 'string' ? fields.engine : 'ninfer';
94
+ const config = fields.config && typeof fields.config === 'object' ? fields.config : {};
95
+ const platform = parseStoredPlatformConfig(fields.platform) ?? {};
96
+ const provider = { id: fields.id, engine, config, platform };
97
+ const launch = parseLaunchRecord(fields.launch);
98
+ if (launch !== undefined) {
99
+ provider.launch = launch;
100
+ }
101
+ providers.push(provider);
102
+ }
103
+ return providers;
104
+ }
105
+ /**
106
+ * parseLaunchRecord reads one stored launch record, keeping only the fields
107
+ * the re-link rule prices (the engine pid, the start time, the argv, the
108
+ * parent and previous pids, and the engine/model/port the detector ties the
109
+ * engine to). A record that is not an object, or that names no integer pid
110
+ * or engine id, is dropped, so a by-hand edit never poisons the re-link.
111
+ */
112
+ function parseLaunchRecord(raw) {
113
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
114
+ return undefined;
115
+ }
116
+ const fields = raw;
117
+ if (typeof fields.pid !== 'number' || !Number.isInteger(fields.pid)) {
118
+ return undefined;
119
+ }
120
+ if (typeof fields.startedAt !== 'string' || fields.startedAt === '') {
121
+ return undefined;
122
+ }
123
+ if (typeof fields.engineId !== 'string' || fields.engineId.trim() === '') {
124
+ return undefined;
125
+ }
126
+ const record = {
127
+ pid: fields.pid,
128
+ startedAt: fields.startedAt,
129
+ argv: Array.isArray(fields.argv)
130
+ ? fields.argv.filter((part) => typeof part === 'string')
131
+ : [],
132
+ previousPids: Array.isArray(fields.previousPids)
133
+ ? fields.previousPids.filter((part) => typeof part === 'number')
134
+ : [],
135
+ engineId: fields.engineId,
136
+ };
137
+ if (typeof fields.parentPid === 'number') {
138
+ record.parentPid = fields.parentPid;
139
+ }
140
+ if (typeof fields.modelId === 'string' && fields.modelId !== '') {
141
+ record.modelId = fields.modelId;
142
+ }
143
+ if (typeof fields.port === 'number') {
144
+ record.port = fields.port;
145
+ }
146
+ return record;
147
+ }
148
+ /**
149
+ * writeConfig persists the engine, config, trust gate, per-engine platform
150
+ * configs, and the help toggle, creating the directory tree on first write.
151
+ */
152
+ export function writeConfig(engine, config, allowRunDeps, platform, showHelp, providers, logPath, lastLogs) {
153
+ const path = localInferConfigPath();
154
+ mkdirSync(dirname(path), { recursive: true });
155
+ writeFileSync(path, JSON.stringify({
156
+ engine,
157
+ config,
158
+ allowRunDeps,
159
+ platform: platform ?? {},
160
+ showHelp: showHelp ?? true,
161
+ providers: providers ?? [],
162
+ logPath: logPath ?? DEFAULT_LOG_PATH_TEMPLATE,
163
+ lastLogs: lastLogs ?? '',
164
+ }, null, 2));
165
+ }
166
+ /**
167
+ * parsePlatformConfigs reads the stored platform map, keeping only entries
168
+ * whose fields are strings. A non-object map or a non-object entry reads as
169
+ * absent, so the deploy step never sees a platform config it cannot price.
170
+ */
171
+ export function parsePlatformConfigs(raw) {
172
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
173
+ return {};
174
+ }
175
+ const platform = {};
176
+ for (const [engineId, entry] of Object.entries(raw)) {
177
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
178
+ continue;
179
+ }
180
+ const stored = {};
181
+ const fields = entry;
182
+ if (typeof fields.engineDirectory === 'string') {
183
+ stored.engineDirectory = fields.engineDirectory;
184
+ }
185
+ if (typeof fields.engineFork === 'string') {
186
+ stored.engineFork = fields.engineFork;
187
+ }
188
+ if (typeof fields.buildCommands === 'string') {
189
+ stored.buildCommands = fields.buildCommands;
190
+ }
191
+ if (typeof fields.dockerTag === 'string') {
192
+ stored.dockerTag = fields.dockerTag;
193
+ }
194
+ if (stored.engineDirectory !== undefined ||
195
+ stored.engineFork !== undefined ||
196
+ stored.buildCommands !== undefined ||
197
+ stored.dockerTag !== undefined) {
198
+ platform[engineId] = stored;
199
+ }
200
+ }
201
+ return platform;
202
+ }
203
+ /**
204
+ * parseStoredPlatformConfig reads one engine's platform config (the launch
205
+ * request's `platform` key), keeping only string fields. A non-object or an
206
+ * object with no string fields reads as absent, so the launch keeps the stored
207
+ * entry the save route synced.
208
+ */
209
+ export function parseStoredPlatformConfig(raw) {
210
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
211
+ return undefined;
212
+ }
213
+ const fields = raw;
214
+ const stored = {};
215
+ if (typeof fields.engineDirectory === 'string') {
216
+ stored.engineDirectory = fields.engineDirectory;
217
+ }
218
+ if (typeof fields.engineFork === 'string') {
219
+ stored.engineFork = fields.engineFork;
220
+ }
221
+ if (typeof fields.buildCommands === 'string') {
222
+ stored.buildCommands = fields.buildCommands;
223
+ }
224
+ if (typeof fields.dockerTag === 'string') {
225
+ stored.dockerTag = fields.dockerTag;
226
+ }
227
+ if (stored.engineDirectory === undefined &&
228
+ stored.engineFork === undefined &&
229
+ stored.buildCommands === undefined &&
230
+ stored.dockerTag === undefined) {
231
+ return undefined;
232
+ }
233
+ return stored;
234
+ }
235
+ //# sourceMappingURL=config.js.map
@@ -7,7 +7,7 @@
7
7
  * here.
8
8
  * @module dsh-plugin-local-infer/ui/deps
9
9
  */
10
- import type { DepCheck, DepStatus } from 'dsh-plugin-local-infer/core';
10
+ import type { BuildMode, DepCheck, DepStatus } from 'dsh-plugin-local-infer/core';
11
11
  /**
12
12
  * DepItemModel is one row of the panel's dep list: the dep id, the purpose
13
13
  * the user reads, the current status, and whether installing it needs sudo.
@@ -40,7 +40,7 @@ export interface ChatTemplateDepModel {
40
40
  */
41
41
  export interface DepsModel {
42
42
  engineId: string;
43
- buildMode: 'docker' | 'local';
43
+ buildMode: BuildMode;
44
44
  packageManager: string | null;
45
45
  deps: DepItemModel[];
46
46
  allSatisfied: boolean;
@@ -56,7 +56,7 @@ export interface DepsModel {
56
56
  */
57
57
  export declare function renderDeps(input: {
58
58
  engineId: string;
59
- buildMode: 'docker' | 'local';
59
+ buildMode: BuildMode;
60
60
  packageManager: string | null;
61
61
  checks: DepCheck[];
62
62
  installString: string;