@qvac/harness 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +31 -2
  4. package/dist-lib/index.js +365 -53
  5. package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
  6. package/dist-lib/lib/audio-transcode-bare.js +22 -25
  7. package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
  8. package/dist-lib/lib/audio-transcode-node.js +1 -3
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
  10. package/dist-lib/lib/capabilities/audio-resample.js +7 -8
  11. package/dist-lib/lib/capabilities/completion.d.ts +5 -2
  12. package/dist-lib/lib/capabilities/completion.js +22 -4
  13. package/dist-lib/lib/capabilities/download.d.ts +3 -1
  14. package/dist-lib/lib/capabilities/download.js +1 -1
  15. package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
  16. package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
  17. package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
  18. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  19. package/dist-lib/lib/capabilities/voice.js +94 -26
  20. package/dist-lib/lib/connect.d.ts +4 -1
  21. package/dist-lib/lib/connect.js +14 -1
  22. package/dist-lib/lib/contract-types.d.ts +10 -0
  23. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  24. package/dist-lib/lib/models/download-gate.js +73 -0
  25. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  26. package/dist-lib/lib/models/memory-cap.js +21 -0
  27. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  28. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  29. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  30. package/dist-lib/lib/models/model-catalog.js +35 -3
  31. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  32. package/dist-lib/lib/models/model-handle.js +20 -2
  33. package/dist-lib/lib/models/model-list.d.ts +8 -1
  34. package/dist-lib/lib/models/model-list.js +23 -0
  35. package/dist-lib/lib/models/model-pool.d.ts +33 -5
  36. package/dist-lib/lib/models/model-pool.js +97 -29
  37. package/dist-lib/lib/serve.js +18 -2
  38. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  39. package/dist-lib/lib/tools/generate-image.js +5 -2
  40. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  41. package/dist-lib/lib/tools/generate-music.js +5 -2
  42. package/dist-lib/lib/tools/translate.d.ts +3 -1
  43. package/dist-lib/lib/tools/translate.js +6 -1
  44. package/dist-lib/schema/harness.js +41 -3
  45. package/dist-lib/spec/bind.js +9 -0
  46. package/dist-lib/spec/calls.js +3 -0
  47. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  48. package/dist-lib/spec/hrpc/index.js +39 -3
  49. package/dist-lib/spec/hrpc/messages.js +93 -10
  50. package/dist-lib/spec/hyperschema/index.js +93 -10
  51. package/dist-lib/spec/hyperschema/schema.json +57 -1
  52. package/dist-lib/spec/hyperschema/types.d.ts +19 -2
  53. package/package.json +4 -4
@@ -11,6 +11,7 @@ import { RUNAWAY_STOP_SEQUENCES } from '../helpers/control-tokens.js';
11
11
  import { raceTimeout } from '../helpers/race-timeout.js';
12
12
  import { resolveModelSrc } from './model-source.js';
13
13
  import { errorMessage } from '../guards.js';
14
+ import { AbortedError } from '../helpers/race-abort.js';
14
15
  export function createLoadTracker() {
15
16
  const inflight = new Set();
16
17
  let draining = null;
@@ -73,10 +74,14 @@ function entryOf(pool, key) {
73
74
  export function modelOf(key) {
74
75
  return key.split('\0')[0];
75
76
  }
77
+ let uses = 0;
76
78
  export function releaseModel(pool, key) {
77
79
  if (pool.destroyed)
78
80
  return;
79
81
  const handle = entryOf(pool, key)?.value;
82
+ if (handle) {
83
+ handle.lastUsed = ++uses;
84
+ }
80
85
  if (handle === undefined || !handle.loading) {
81
86
  pool.release(key);
82
87
  return;
@@ -86,8 +91,18 @@ export function releaseModel(pool, key) {
86
91
  pool.release(key);
87
92
  return;
88
93
  }
89
- void pending.then(() => releaseSettled(pool, key), () => releaseSettled(pool, key));
94
+ const deferred = (deferredReleases.get(handle) ?? 0) + 1;
95
+ deferredReleases.set(handle, deferred);
96
+ if (deferred >= (entryOf(pool, key)?.refs ?? 0)) {
97
+ handle.abandon();
98
+ }
99
+ const settle = () => {
100
+ deferredReleases.set(handle, (deferredReleases.get(handle) ?? 1) - 1);
101
+ releaseSettled(pool, key);
102
+ };
103
+ void pending.then(settle, settle);
90
104
  }
105
+ const deferredReleases = new WeakMap();
91
106
  function releaseSettled(pool, key) {
92
107
  if (!pool.destroyed) {
93
108
  pool.release(key);
@@ -111,6 +126,21 @@ export function settledModels(pool) {
111
126
  }
112
127
  return out;
113
128
  }
129
+ export function settledEntries(pool) {
130
+ const out = [];
131
+ for (const entry of pool.resources.values()) {
132
+ const resource = entry.value.resource;
133
+ if (resource !== null) {
134
+ out.push({
135
+ key: entry.id,
136
+ refs: entry.refs,
137
+ bytes: resource.bytes,
138
+ lastUsed: entry.value.lastUsed
139
+ });
140
+ }
141
+ }
142
+ return out;
143
+ }
114
144
  export function loadingModels(pool) {
115
145
  const out = [];
116
146
  for (const entry of pool.resources.values()) {
@@ -120,14 +150,19 @@ export function loadingModels(pool) {
120
150
  }
121
151
  return out;
122
152
  }
123
- function reportedHandle(notify, model, load) {
153
+ function reportedHandle({ notify, model, addon, hold, load }) {
124
154
  return new ModelHandle({
125
- load: () => {
155
+ load: async (signal) => {
156
+ await hold?.(model, addon, signal);
126
157
  notify({ status: 'loading', model, catalogChanged: true });
127
158
  return load();
128
159
  },
129
160
  onLoaded: () => notify({ status: 'ready', model, catalogChanged: true }),
130
- onFailed: (err) => notify({ status: 'error', model, message: errorMessage(err), catalogChanged: true })
161
+ onFailed: (err) => {
162
+ if (err instanceof AbortedError)
163
+ return;
164
+ notify({ status: 'error', model, message: errorMessage(err), catalogChanged: true });
165
+ }
131
166
  });
132
167
  }
133
168
  export async function leaseResidentModel(pool, wanted = () => true) {
@@ -226,6 +261,10 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
226
261
  export function evictModelWithin(pool, key, options) {
227
262
  return evictWhereWithin(pool, (candidate) => candidate === key, options);
228
263
  }
264
+ export function evictIdleWithin(pool, key, { deadlineMs }) {
265
+ const evicting = evictIdleWhere(pool, (candidate) => candidate === key).then(() => !isResident(pool, key));
266
+ return raceTimeout(evicting, { ms: deadlineMs, fallback: false });
267
+ }
229
268
  export function evictModel(pool, model, options) {
230
269
  return evictWhereWithin(pool, (key) => modelOf(key) === model, options);
231
270
  }
@@ -318,8 +357,8 @@ export function loadClockGuard(model, sdk, { now = () => Date.now(), monoMs = mo
318
357
  };
319
358
  return { race };
320
359
  }
321
- export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = createLoadTracker() } = {}) {
322
- const load = async (model, opts) => {
360
+ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = createLoadTracker(), beforeLoad, hold } = {}) {
361
+ const load = async (key, model, opts) => {
323
362
  const sdk = await loadSdk();
324
363
  const modelSrc = await resolveModelSrc(sdk, model);
325
364
  const resolved = resolveLoadOptions(opts, deviceConfig);
@@ -352,54 +391,80 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
352
391
  });
353
392
  const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
354
393
  hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
394
+ const admission = await beforeLoad?.({
395
+ key,
396
+ model,
397
+ modelSrc,
398
+ modelType: 'llamacpp-completion',
399
+ modelConfig
400
+ });
355
401
  const loadStarted = Date.now();
356
- const modelId = await loadClockGuard(model, sdk).race(loads.track(sdk.loadModel({
402
+ const modelId = await loadClockGuard(model, sdk)
403
+ .race(loads.track(sdk.loadModel({
357
404
  modelSrc,
358
405
  modelType: 'llamacpp-completion',
359
406
  modelConfig,
360
407
  onProgress: (progress) => notify(progressState(model, progress))
361
- })));
408
+ })))
409
+ .finally(() => admission?.release());
362
410
  hlog('llm loaded', { model, loadMs: Date.now() - loadStarted });
363
- return { model, modelId, ctxSize };
411
+ return { model, modelId, ctxSize, bytes: admission?.bytes };
364
412
  };
365
413
  return new ModelPuddle({
366
- open: (key, model = key, opts = {}) => reportedHandle(notify, model, () => load(model, opts)),
414
+ open: (key, model = key, opts = {}) => reportedHandle({ notify, model, addon: 'llm', hold, load: () => load(key, model, opts) }),
367
415
  gc: unloadOnGc(loadSdk, notify),
368
416
  ...NO_IDLE_GC
369
417
  });
370
418
  }
371
- export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_EMBED_BATCH_SIZE, openclCacheDir, loads = createLoadTracker() }) {
372
- const load = async (model) => {
419
+ export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_EMBED_BATCH_SIZE, openclCacheDir, loads = createLoadTracker(), beforeLoad, hold }) {
420
+ const load = async (key, model) => {
373
421
  const sdk = await loadSdk();
374
422
  const modelSrc = await resolveModelSrc(sdk, model, 'embeddings');
375
423
  const openclCache = faults.device !== 'cpu' && openclCacheDir ? openclCacheDir() : undefined;
376
- const modelId = await loadClockGuard(model, sdk).race(loads.track(sdk.loadModel({
424
+ const modelConfig = {
425
+ batchSize,
426
+ gpuLayers: faults.gpuLayers(),
427
+ device: faults.device,
428
+ openclCacheDir: openclCache || undefined
429
+ };
430
+ const admission = await beforeLoad?.({
431
+ key,
432
+ model,
377
433
  modelSrc,
378
434
  modelType: 'llamacpp-embedding',
379
- modelConfig: {
380
- batchSize,
381
- gpuLayers: faults.gpuLayers(),
382
- device: faults.device,
383
- openclCacheDir: openclCache || undefined
384
- },
435
+ modelConfig
436
+ });
437
+ const modelId = await loadClockGuard(model, sdk)
438
+ .race(loads.track(sdk.loadModel({
439
+ modelSrc,
440
+ modelType: 'llamacpp-embedding',
441
+ modelConfig,
385
442
  onProgress: (progress) => notify(progressState(model, progress))
386
- })));
387
- return { model, modelId };
443
+ })))
444
+ .finally(() => admission?.release());
445
+ return { model, modelId, bytes: admission?.bytes };
388
446
  };
389
447
  return new ModelPuddle({
390
- open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
448
+ open: (key, model = key) => reportedHandle({ notify, model, addon: 'embeddings', hold, load: () => load(key, model) }),
391
449
  gc: unloadOnGc(loadSdk, notify),
392
450
  ...NO_IDLE_GC
393
451
  });
394
452
  }
395
- export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads = createLoadTracker() }) {
453
+ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads = createLoadTracker(), beforeLoad, hold }) {
396
454
  const wantsGpu = modelConfig?.['useGPU'] === true;
397
455
  let gpuRefused = false;
398
- const load = async (model) => {
456
+ const load = async (key, model) => {
399
457
  const sdk = await loadSdk();
400
458
  const modelSrc = await resolveModelSrc(sdk, model, addon);
401
459
  const tuned = modelConfigFor?.(modelSrc);
402
460
  const loadConfig = tuned ? { ...modelConfig, ...tuned } : modelConfig;
461
+ const admission = await beforeLoad?.({
462
+ key,
463
+ model,
464
+ modelSrc,
465
+ modelType,
466
+ modelConfig: loadConfig
467
+ });
403
468
  const attempt = () => {
404
469
  const config = gpuRefused ? { ...loadConfig, useGPU: false } : loadConfig;
405
470
  const openclCache = gpuRefused ? undefined : openclCacheDir?.();
@@ -410,17 +475,19 @@ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig
410
475
  onProgress: (progress) => notify(progressState(model, progress))
411
476
  }));
412
477
  };
413
- const modelId = await loadClockGuard(model, sdk).race(attempt().catch((err) => {
478
+ const modelId = await loadClockGuard(model, sdk)
479
+ .race(attempt().catch((err) => {
414
480
  if (!wantsGpu || gpuRefused || loads.draining)
415
481
  throw err;
416
482
  hlog('voice gpu load refused, retrying on cpu', { model, modelType, error: String(err) });
417
483
  gpuRefused = true;
418
484
  return attempt();
419
- }));
420
- return { model, modelId };
485
+ }))
486
+ .finally(() => admission?.release());
487
+ return { model, modelId, bytes: admission?.bytes };
421
488
  };
422
489
  return new ModelPuddle({
423
- open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
490
+ open: (key, model = key) => reportedHandle({ notify, model, addon, hold, load: () => load(key, model) }),
424
491
  gc: unloadOnGc(loadSdk, notify),
425
492
  ...NO_IDLE_GC
426
493
  });
@@ -445,7 +512,8 @@ function unloadOnGc(loadSdk, notify) {
445
512
  try {
446
513
  await sdk.unloadModel({ modelId });
447
514
  }
448
- catch {
515
+ catch (err) {
516
+ hlog('unload failed', { model, err: errorMessage(err) });
449
517
  continue;
450
518
  }
451
519
  notify({ status: 'unloaded', model, catalogChanged: true });
@@ -23,7 +23,8 @@ const MODEL_STATUSES = new Set([
23
23
  'ready',
24
24
  'error',
25
25
  'unloading',
26
- 'unloaded'
26
+ 'unloaded',
27
+ 'held'
27
28
  ]);
28
29
  const DOWNLOAD_PROGRESS_INTERVAL_MS = 500;
29
30
  const WIRE_HIGH_WATER_MARK = 64;
@@ -50,6 +51,20 @@ export function serveHarness(stream, harness) {
50
51
  await harness.setModelIdleTimeout?.(ms);
51
52
  return { ok: true };
52
53
  },
54
+ setModelMemoryCap: async ({ percent }) => {
55
+ await harness.setModelMemoryCap?.(percent);
56
+ return { ok: true };
57
+ },
58
+ memoryUsage: async () => {
59
+ if (!harness.memoryUsage) {
60
+ throw new Error('harness does not support memoryUsage');
61
+ }
62
+ return harness.memoryUsage();
63
+ },
64
+ setDownloadsAllowed: async ({ allowed }) => {
65
+ await harness.setDownloadsAllowed?.({ allowed: allowed === true });
66
+ return { ok: true };
67
+ },
53
68
  listManagedSkills: async () => ({
54
69
  skills: (await harness.listManagedSkills?.()) ?? []
55
70
  }),
@@ -282,7 +297,8 @@ function modelEvents(harness) {
282
297
  total,
283
298
  message: state.message,
284
299
  catalogChanged,
285
- loaded
300
+ loaded,
301
+ addon: state.addon
286
302
  };
287
303
  const now = Date.now();
288
304
  if (state.status === 'downloading') {
@@ -3,6 +3,7 @@ import type { ToolDefinition } from '../capabilities/completion.ts';
3
3
  import type { ModelSrc } from '../models/context.ts';
4
4
  import { type ImageClamps } from '../models/model-catalog.ts';
5
5
  import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
6
+ import type { HoldFn } from '../models/download-gate.ts';
6
7
  import { type ModelRegistrySdk } from '../models/model-source.ts';
7
8
  export declare const MAX_DIM = 1024;
8
9
  export declare const DIM_STEP = 64;
@@ -68,7 +69,8 @@ export interface DiffusionRuntimeOptions extends ImageModelSet {
68
69
  loadSdk: () => Promise<DiffusionSdk>;
69
70
  onProgress?: (model: string, progress: Record<string, unknown>) => void;
70
71
  onLoaded?: (model: string) => void;
71
- onBeforeLoad?: (signal?: BareAbortSignal) => Promise<void>;
72
+ onBeforeLoad?: (signal: BareAbortSignal | undefined, srcs: ReadonlyArray<string>) => Promise<void>;
73
+ hold?: HoldFn;
72
74
  loads?: LoadTracker;
73
75
  stallMs?: number;
74
76
  }
@@ -87,7 +89,7 @@ export interface DiffusionRuntime {
87
89
  }
88
90
  export declare function generationPercentTick(onPercent: (percentage: number) => void, encodePhases?: number): (tick: DiffusionTick) => void;
89
91
  export declare const IMAGE_LOAD_STALL_ERROR = "image model download stalled";
90
- export declare function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, loads, stallMs, ...base }: DiffusionRuntimeOptions): DiffusionRuntime;
92
+ export declare function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, hold, loads, stallMs, ...base }: DiffusionRuntimeOptions): DiffusionRuntime;
91
93
  export declare const BUSY_ERROR = "an image generation is already running; try again when it finishes";
92
94
  export interface ImageToolError {
93
95
  error: string;
@@ -39,7 +39,7 @@ function diffusionProgressName(set) {
39
39
  return entry?.name ?? set.model;
40
40
  }
41
41
  export const IMAGE_LOAD_STALL_ERROR = 'image model download stalled';
42
- export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, loads = createLoadTracker(), stallMs = IMAGE_LOAD_STALL_MS, ...base }) {
42
+ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, hold, loads = createLoadTracker(), stallMs = IMAGE_LOAD_STALL_MS, ...base }) {
43
43
  let loading = null;
44
44
  let residentFingerprint = null;
45
45
  let busy = false;
@@ -76,13 +76,16 @@ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBefore
76
76
  ensureLoaded: async (sdk, signal, modelChoice) => {
77
77
  const set = (modelChoice && imageModelFor(modelChoice)) || base;
78
78
  const fingerprint = fingerprintOf(set);
79
+ if (hold && !(loading && residentFingerprint === fingerprint)) {
80
+ await hold(diffusionProgressName(set), 'diffusion', signal);
81
+ }
79
82
  await lock.lock();
80
83
  try {
81
84
  if (loading && residentFingerprint !== fingerprint) {
82
85
  await doUnload();
83
86
  }
84
87
  if (!loading && onBeforeLoad) {
85
- await onBeforeLoad(signal);
88
+ await onBeforeLoad(signal, [set.model, set.llmModel, set.vaeModel].filter((src) => src !== undefined));
86
89
  }
87
90
  residentFingerprint = fingerprint;
88
91
  loading ??= (async () => {
@@ -2,6 +2,7 @@ import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
2
  import type { MusicSettings, ToolDefinition, ToolExecuteContext } from '../capabilities/completion.ts';
3
3
  import type { ModelSrc } from '../models/context.ts';
4
4
  import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
5
+ import type { HoldFn } from '../models/download-gate.ts';
5
6
  import { type ModelRegistrySdk } from '../models/model-source.ts';
6
7
  export declare const MAX_DURATION_SECONDS = 300;
7
8
  export declare const MIN_DURATION_SECONDS = 1;
@@ -75,7 +76,8 @@ export interface MusicRuntimeOptions extends MusicModelStages {
75
76
  inferenceSteps?: number;
76
77
  useGpu?: boolean;
77
78
  onProgress?: (model: string, progress: Record<string, unknown>) => void;
78
- onBeforeLoad?: (signal?: BareAbortSignal) => Promise<void>;
79
+ onBeforeLoad?: (signal: BareAbortSignal | undefined, srcs: ReadonlyArray<string>) => Promise<void>;
80
+ hold?: HoldFn;
79
81
  loads?: LoadTracker;
80
82
  }
81
83
  export interface LoadedMusicModel {
@@ -97,7 +99,7 @@ export interface MusicRuntime {
97
99
  release(): void;
98
100
  }
99
101
  export declare function musicPercentTick(onPercent: (percentage: number) => void): (tick: MusicTick) => void;
100
- export declare function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads, inferenceSteps: baseSteps, useGpu, ...base }: MusicRuntimeOptions): MusicRuntime;
102
+ export declare function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, hold, loads, inferenceSteps: baseSteps, useGpu, ...base }: MusicRuntimeOptions): MusicRuntime;
101
103
  export declare const BUSY_ERROR = "a music generation is already running; try again when it finishes";
102
104
  export interface MusicToolError {
103
105
  error: string;
@@ -49,7 +49,7 @@ function musicProgressName(dit) {
49
49
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.dit.src === dit || candidate.dit.name === dit);
50
50
  return entry?.name ?? dit;
51
51
  }
52
- export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads = createLoadTracker(), inferenceSteps: baseSteps, useGpu = true, ...base }) {
52
+ export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, hold, loads = createLoadTracker(), inferenceSteps: baseSteps, useGpu = true, ...base }) {
53
53
  let loading = null;
54
54
  let residentFingerprint = null;
55
55
  let busy = false;
@@ -89,13 +89,16 @@ export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads =
89
89
  const stages = stagesFor(modelChoice, base);
90
90
  const effectiveSteps = steps ?? baseSteps;
91
91
  const fingerprint = fingerprintOf(stages, effectiveSteps);
92
+ if (hold && !(loading && residentFingerprint === fingerprint)) {
93
+ await hold(musicProgressName(stages.dit), 'audiogen', signal);
94
+ }
92
95
  await lock.lock();
93
96
  try {
94
97
  if (loading && residentFingerprint !== fingerprint) {
95
98
  await doUnload();
96
99
  }
97
100
  if (!loading && onBeforeLoad) {
98
- await onBeforeLoad(signal);
101
+ await onBeforeLoad(signal, [stages.dit, stages.textEnc, stages.lm, stages.vae]);
99
102
  }
100
103
  residentFingerprint = fingerprint;
101
104
  loading ??= (async () => {
@@ -3,6 +3,7 @@ import type { ToolDefinition } from '../capabilities/completion.ts';
3
3
  import type { ModelSrc } from '../models/context.ts';
4
4
  import { type NmtPlan } from '../models/nmt-catalog.ts';
5
5
  import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
6
+ import type { HoldFn } from '../models/download-gate.ts';
6
7
  import { type ModelRegistrySdk } from '../models/model-source.ts';
7
8
  interface BergamotConfig {
8
9
  engine: 'Bergamot';
@@ -42,6 +43,7 @@ export interface TranslationRuntimeOptions {
42
43
  loadSdk: () => Promise<TranslationSdk>;
43
44
  loads?: LoadTracker;
44
45
  onProgress?: (model: string, progress: Record<string, unknown>) => void;
46
+ hold?: HoldFn;
45
47
  stallMs?: number;
46
48
  }
47
49
  export interface TranslationRuntime {
@@ -53,7 +55,7 @@ export interface TranslationRuntime {
53
55
  enter: (signal?: BareAbortSignal) => Promise<void>;
54
56
  exit: () => void;
55
57
  }
56
- export declare function createTranslationRuntime({ loadSdk, loads, onProgress, stallMs }: TranslationRuntimeOptions): TranslationRuntime;
58
+ export declare function createTranslationRuntime({ loadSdk, loads, onProgress, hold, stallMs }: TranslationRuntimeOptions): TranslationRuntime;
57
59
  export type SlmTranslateFn = (model: string, params: {
58
60
  text: string;
59
61
  from: string;
@@ -26,7 +26,7 @@ function bergamotConfig(plan) {
26
26
  }
27
27
  return config;
28
28
  }
29
- export function createTranslationRuntime({ loadSdk, loads = createLoadTracker(), onProgress, stallMs = TRANSLATION_LOAD_STALL_MS }) {
29
+ export function createTranslationRuntime({ loadSdk, loads = createLoadTracker(), onProgress, hold, stallMs = TRANSLATION_LOAD_STALL_MS }) {
30
30
  let loading = null;
31
31
  let residentFingerprint = null;
32
32
  const lock = new ScopeLock();
@@ -95,6 +95,11 @@ export function createTranslationRuntime({ loadSdk, loads = createLoadTracker(),
95
95
  loadSdk,
96
96
  ensureLoaded: async (sdk, plan, signal) => {
97
97
  const fingerprint = fingerprintOf(plan);
98
+ if (hold && !(loading && residentFingerprint === fingerprint)) {
99
+ for (const model of plan.pivot ? [plan.model, plan.pivot] : [plan.model]) {
100
+ await hold(model, 'nmt', signal);
101
+ }
102
+ }
98
103
  await lock.lock();
99
104
  try {
100
105
  if (loading && residentFingerprint !== fingerprint) {
@@ -175,7 +175,16 @@ export function registerHarnessTypes(ns) {
175
175
  });
176
176
  ns.register({
177
177
  name: 'model-event-status',
178
- enum: ['loading', 'downloading', 'ready', 'error', 'unloading', 'unloaded', 'downloaded'],
178
+ enum: [
179
+ 'loading',
180
+ 'downloading',
181
+ 'ready',
182
+ 'error',
183
+ 'unloading',
184
+ 'unloaded',
185
+ 'downloaded',
186
+ 'held'
187
+ ],
179
188
  strings: true
180
189
  });
181
190
  ns.register({
@@ -195,7 +204,8 @@ export function registerHarnessTypes(ns) {
195
204
  { name: 'loaded', type: '@harness/loaded-model', array: true },
196
205
  { name: 'catalogChanged', type: 'bool' },
197
206
  { name: 'downloaded', type: 'uint' },
198
- { name: 'total', type: 'uint' }
207
+ { name: 'total', type: 'uint' },
208
+ { name: 'addon', type: 'string' }
199
209
  ]
200
210
  });
201
211
  ns.register({
@@ -340,7 +350,8 @@ export function registerHarnessTypes(ns) {
340
350
  { name: 'expectedSize', type: 'uint', required: true },
341
351
  { name: 'actualSize', type: 'uint' },
342
352
  { name: 'label', type: 'string' },
343
- { name: 'isLoading', type: 'bool' }
353
+ { name: 'isLoading', type: 'bool' },
354
+ { name: 'memoryBytes', type: 'uint' }
344
355
  ]
345
356
  });
346
357
  ns.register({
@@ -738,6 +749,18 @@ export function registerHarnessTypes(ns) {
738
749
  name: 'unload-model-response',
739
750
  fields: [{ name: 'unloaded', type: 'bool', required: true }]
740
751
  });
752
+ ns.register({
753
+ name: 'set-model-memory-cap-request',
754
+ fields: [{ name: 'percent', type: 'uint', required: true }]
755
+ });
756
+ ns.register({
757
+ name: 'memory-usage',
758
+ fields: [{ name: 'usedBytes', type: 'uint', required: true }]
759
+ });
760
+ ns.register({
761
+ name: 'set-downloads-allowed-request',
762
+ fields: [{ name: 'allowed', type: 'bool', required: true }]
763
+ });
741
764
  }
742
765
  export function registerHarnessApi(api) {
743
766
  api.register({
@@ -905,4 +928,19 @@ export function registerHarnessApi(api) {
905
928
  request: { name: '@harness/unload-model-request', stream: false },
906
929
  response: { name: '@harness/unload-model-response', stream: false }
907
930
  });
931
+ api.register({
932
+ name: 'set-model-memory-cap',
933
+ request: { name: '@harness/set-model-memory-cap-request', stream: false },
934
+ response: { name: '@harness/ok-response', stream: false }
935
+ });
936
+ api.register({
937
+ name: 'memory-usage',
938
+ request: { name: '@harness/empty', stream: false },
939
+ response: { name: '@harness/memory-usage', stream: false }
940
+ });
941
+ api.register({
942
+ name: 'set-downloads-allowed',
943
+ request: { name: '@harness/set-downloads-allowed-request', stream: false },
944
+ response: { name: '@harness/ok-response', stream: false }
945
+ });
908
946
  }
@@ -139,4 +139,13 @@ export function bindApi(rpc, api) {
139
139
  if (api.unloadModel) {
140
140
  rpc.onUnloadModel((req) => api.unloadModel(req));
141
141
  }
142
+ if (api.setModelMemoryCap) {
143
+ rpc.onSetModelMemoryCap((req) => api.setModelMemoryCap(req));
144
+ }
145
+ if (api.memoryUsage) {
146
+ rpc.onMemoryUsage((req) => api.memoryUsage(req));
147
+ }
148
+ if (api.setDownloadsAllowed) {
149
+ rpc.onSetDownloadsAllowed((req) => api.setDownloadsAllowed(req));
150
+ }
142
151
  }
@@ -43,5 +43,8 @@ export function harnessClient(rpc) {
43
43
  setModelIdleTimeout: (input) => rpc.setModelIdleTimeout(input),
44
44
  loadModel: (input) => rpc.loadModel(input),
45
45
  unloadModel: (input) => rpc.unloadModel(input),
46
+ setModelMemoryCap: (input) => rpc.setModelMemoryCap(input),
47
+ memoryUsage: (input) => rpc.memoryUsage(input),
48
+ setDownloadsAllowed: (input) => rpc.setDownloadsAllowed(input),
46
49
  };
47
50
  }
@@ -429,6 +429,45 @@
429
429
  "stream": false
430
430
  },
431
431
  "version": 1
432
+ },
433
+ {
434
+ "id": 33,
435
+ "name": "@harness/set-model-memory-cap",
436
+ "request": {
437
+ "name": "@harness/set-model-memory-cap-request",
438
+ "stream": false
439
+ },
440
+ "response": {
441
+ "name": "@harness/ok-response",
442
+ "stream": false
443
+ },
444
+ "version": 1
445
+ },
446
+ {
447
+ "id": 34,
448
+ "name": "@harness/memory-usage",
449
+ "request": {
450
+ "name": "@harness/empty",
451
+ "stream": false
452
+ },
453
+ "response": {
454
+ "name": "@harness/memory-usage",
455
+ "stream": false
456
+ },
457
+ "version": 1
458
+ },
459
+ {
460
+ "id": 35,
461
+ "name": "@harness/set-downloads-allowed",
462
+ "request": {
463
+ "name": "@harness/set-downloads-allowed-request",
464
+ "stream": false
465
+ },
466
+ "response": {
467
+ "name": "@harness/ok-response",
468
+ "stream": false
469
+ },
470
+ "version": 1
432
471
  }
433
472
  ]
434
473
  }
@@ -71,7 +71,13 @@ const methods = new Map([
71
71
  ['@harness/load-model', 31],
72
72
  [31, '@harness/load-model'],
73
73
  ['@harness/unload-model', 32],
74
- [32, '@harness/unload-model']
74
+ [32, '@harness/unload-model'],
75
+ ['@harness/set-model-memory-cap', 33],
76
+ [33, '@harness/set-model-memory-cap'],
77
+ ['@harness/memory-usage', 34],
78
+ [34, '@harness/memory-usage'],
79
+ ['@harness/set-downloads-allowed', 35],
80
+ [35, '@harness/set-downloads-allowed']
75
81
  ])
76
82
 
77
83
  class HRPC {
@@ -111,7 +117,10 @@ class HRPC {
111
117
  ['@harness/preview-model-source', getEncoding('@harness/preview-model-source-request')],
112
118
  ['@harness/set-model-idle-timeout', getEncoding('@harness/set-model-idle-timeout-request')],
113
119
  ['@harness/load-model', getEncoding('@harness/warm-model-request')],
114
- ['@harness/unload-model', getEncoding('@harness/unload-model-request')]
120
+ ['@harness/unload-model', getEncoding('@harness/unload-model-request')],
121
+ ['@harness/set-model-memory-cap', getEncoding('@harness/set-model-memory-cap-request')],
122
+ ['@harness/memory-usage', getEncoding('@harness/empty')],
123
+ ['@harness/set-downloads-allowed', getEncoding('@harness/set-downloads-allowed-request')]
115
124
  ])
116
125
  this._responseEncodings = new Map([
117
126
  ['@harness/completion', getEncoding('@harness/completion-event')],
@@ -146,7 +155,10 @@ class HRPC {
146
155
  ['@harness/preview-model-source', getEncoding('@harness/preview-model-source-response')],
147
156
  ['@harness/set-model-idle-timeout', getEncoding('@harness/ok-response')],
148
157
  ['@harness/load-model', getEncoding('@harness/load-model-response')],
149
- ['@harness/unload-model', getEncoding('@harness/unload-model-response')]
158
+ ['@harness/unload-model', getEncoding('@harness/unload-model-response')],
159
+ ['@harness/set-model-memory-cap', getEncoding('@harness/ok-response')],
160
+ ['@harness/memory-usage', getEncoding('@harness/memory-usage')],
161
+ ['@harness/set-downloads-allowed', getEncoding('@harness/ok-response')]
150
162
  ])
151
163
  this._rpc = new RPC(stream, async (req) => {
152
164
  const command = methods.get(req.command)
@@ -376,6 +388,18 @@ class HRPC {
376
388
  return this._call('@harness/unload-model', args)
377
389
  }
378
390
 
391
+ async setModelMemoryCap(args) {
392
+ return this._call('@harness/set-model-memory-cap', args)
393
+ }
394
+
395
+ async memoryUsage(args) {
396
+ return this._call('@harness/memory-usage', args)
397
+ }
398
+
399
+ async setDownloadsAllowed(args) {
400
+ return this._call('@harness/set-downloads-allowed', args)
401
+ }
402
+
379
403
  onCompletion(responseFn) {
380
404
  this._handlers['@harness/completion'] = responseFn
381
405
  }
@@ -508,6 +532,18 @@ class HRPC {
508
532
  this._handlers['@harness/unload-model'] = responseFn
509
533
  }
510
534
 
535
+ onSetModelMemoryCap(responseFn) {
536
+ this._handlers['@harness/set-model-memory-cap'] = responseFn
537
+ }
538
+
539
+ onMemoryUsage(responseFn) {
540
+ this._handlers['@harness/memory-usage'] = responseFn
541
+ }
542
+
543
+ onSetDownloadsAllowed(responseFn) {
544
+ this._handlers['@harness/set-downloads-allowed'] = responseFn
545
+ }
546
+
511
547
  _requestIsStream(command) {
512
548
  return [
513
549
  '@harness/completion',