@qvac/harness 0.8.5 → 0.8.7

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 (88) hide show
  1. package/CHANGELOG.md +1278 -472
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +42 -2
  4. package/dist-lib/index.js +447 -58
  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 +6 -3
  12. package/dist-lib/lib/capabilities/completion.js +96 -49
  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/tool-selection.d.ts +1 -3
  18. package/dist-lib/lib/capabilities/tool-selection.js +2 -4
  19. package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
  20. package/dist-lib/lib/capabilities/tool-turns.js +0 -6
  21. package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
  22. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  23. package/dist-lib/lib/capabilities/voice.js +94 -26
  24. package/dist-lib/lib/connect.d.ts +4 -1
  25. package/dist-lib/lib/connect.js +16 -1
  26. package/dist-lib/lib/contract-types.d.ts +12 -0
  27. package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
  28. package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
  29. package/dist-lib/lib/models/context-choice.d.ts +15 -0
  30. package/dist-lib/lib/models/context-choice.js +55 -0
  31. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  32. package/dist-lib/lib/models/download-gate.js +73 -0
  33. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  34. package/dist-lib/lib/models/memory-cap.js +21 -0
  35. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  36. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  37. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  38. package/dist-lib/lib/models/model-catalog.js +35 -3
  39. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  40. package/dist-lib/lib/models/model-handle.js +20 -2
  41. package/dist-lib/lib/models/model-list.d.ts +8 -1
  42. package/dist-lib/lib/models/model-list.js +23 -0
  43. package/dist-lib/lib/models/model-pool.d.ts +35 -5
  44. package/dist-lib/lib/models/model-pool.js +97 -32
  45. package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
  46. package/dist-lib/lib/models/sdk-faults.js +6 -0
  47. package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
  48. package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
  49. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
  50. package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
  51. package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
  52. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
  53. package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
  54. package/dist-lib/lib/serve.js +23 -3
  55. package/dist-lib/lib/spawn.js +4 -10
  56. package/dist-lib/lib/tools/exec.d.ts +4 -1
  57. package/dist-lib/lib/tools/exec.js +55 -11
  58. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  59. package/dist-lib/lib/tools/generate-image.js +5 -2
  60. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  61. package/dist-lib/lib/tools/generate-music.js +5 -2
  62. package/dist-lib/lib/tools/office/ops.js +1 -1
  63. package/dist-lib/lib/tools/office.js +29 -6
  64. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  65. package/dist-lib/lib/tools/skills/catalog.js +2 -0
  66. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  67. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  68. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  69. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  70. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  71. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
  72. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  73. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  74. package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
  75. package/dist-lib/lib/tools/skills/mention.js +0 -3
  76. package/dist-lib/lib/tools/translate.d.ts +3 -1
  77. package/dist-lib/lib/tools/translate.js +6 -1
  78. package/dist-lib/schema/harness.js +43 -4
  79. package/dist-lib/spec/bind.js +9 -0
  80. package/dist-lib/spec/calls.js +3 -0
  81. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  82. package/dist-lib/spec/hrpc/index.js +39 -3
  83. package/dist-lib/spec/hrpc/messages.js +101 -13
  84. package/dist-lib/spec/hyperschema/index.js +101 -13
  85. package/dist-lib/spec/hyperschema/schema.json +62 -1
  86. package/dist-lib/spec/hyperschema/types.d.ts +20 -2
  87. package/package.json +8 -10
  88. package/worker/harness-sidecar.mjs +2 -1
@@ -1,8 +1,8 @@
1
- import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_FP16, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
1
+ import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, MMPROJ_TERNARY_BONSAI_2_27B_MULTIMODAL_Q8_0, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TERNARY_BONSAI_2_27B_MULTIMODAL_PQ2_0, TERNARY_BONSAI_2_27B_MULTIMODAL_PTQ1_0, TTS_MULTILINGUAL_SUPERTONIC3_FP16, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
2
2
  import { detectToolDialect } from './tool-dialect.js';
3
3
  import { defaultTtsModelName } from './tts-model-default.js';
4
- import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName } from './model-catalog-constants.js';
5
- export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName };
4
+ import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, BONSAI_2_27B_PQ2, BONSAI_2_27B_PTQ1, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName } from './model-catalog-constants.js';
5
+ export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, BONSAI_2_27B_PQ2, BONSAI_2_27B_PTQ1, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName };
6
6
  export const LLM_CATALOG = [
7
7
  {
8
8
  name: QWEN_35_2B,
@@ -51,6 +51,20 @@ export const LLM_CATALOG = [
51
51
  weight: QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL,
52
52
  capabilityProfile: 'xhigh',
53
53
  projection: MMPROJ_QWEN3_8_27B_MULTIMODAL_F16
54
+ },
55
+ {
56
+ name: BONSAI_2_27B_PQ2,
57
+ weight: TERNARY_BONSAI_2_27B_MULTIMODAL_PQ2_0,
58
+ capabilityProfile: 'high',
59
+ projection: MMPROJ_TERNARY_BONSAI_2_27B_MULTIMODAL_Q8_0,
60
+ toolDialect: 'qwen35'
61
+ },
62
+ {
63
+ name: BONSAI_2_27B_PTQ1,
64
+ weight: TERNARY_BONSAI_2_27B_MULTIMODAL_PTQ1_0,
65
+ capabilityProfile: 'high',
66
+ projection: MMPROJ_TERNARY_BONSAI_2_27B_MULTIMODAL_Q8_0,
67
+ toolDialect: 'qwen35'
54
68
  }
55
69
  ];
56
70
  export function defaultProjectionMap() {
@@ -181,6 +195,16 @@ export function diffusionDownloadMetadata(model, addon) {
181
195
  assetSizes
182
196
  };
183
197
  }
198
+ export function assetBytes(src) {
199
+ const assets = [
200
+ ...curatedListables().map((entry) => entry.asset),
201
+ ...LLM_CATALOG.flatMap((entry) => (entry.projection ? [entry.projection] : [])),
202
+ ...DIFFUSION_CATALOG.flatMap(diffusionAssets),
203
+ ...AUDIOGEN_CATALOG.flatMap((entry) => [entry.dit, entry.textEnc, entry.lm, entry.vae])
204
+ ];
205
+ const custom = customModels().find((entry) => entry.src === src);
206
+ return assets.find((asset) => asset.src === src)?.expectedSize ?? custom?.expectedSize;
207
+ }
184
208
  import { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom } from './music-catalog.js';
185
209
  export { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom };
186
210
  const MUSIC_GENERATION_SKILL = 'music-generation';
@@ -363,6 +387,11 @@ export function modelAddonOf(model) {
363
387
  export function residentSrc(model, addon) {
364
388
  return curatedModelAsset(model, addon)?.src ?? model;
365
389
  }
390
+ export function catalogModelName(model, addon) {
391
+ const known = curatedListables().find((entry) => entry.asset.addon === addon &&
392
+ (entry.name === model || entry.asset.name === model || entry.asset.src === model));
393
+ return known?.name ?? model;
394
+ }
366
395
  export const CUSTOM_TASK_TYPES = ['llm'];
367
396
  let CUSTOM_MODELS = new Map();
368
397
  export function setCustomModels(entries) {
@@ -378,6 +407,9 @@ export function customModelSrc(name, addon) {
378
407
  const entry = CUSTOM_MODELS.get(name);
379
408
  return entry && (entry.taskType ?? 'llm') === addon ? entry.src : undefined;
380
409
  }
410
+ export function toolDialectFor(name) {
411
+ return LLM_CATALOG.find((entry) => entry.name === name)?.toolDialect ?? customToolDialect(name);
412
+ }
381
413
  export function customToolDialect(name) {
382
414
  const entry = CUSTOM_MODELS.get(name);
383
415
  return entry
@@ -1,10 +1,12 @@
1
+ import { type AbortSignal } from 'bare-abort-controller';
1
2
  export interface ModelResource {
2
3
  model: string;
3
4
  modelId: string;
4
5
  ctxSize?: number;
6
+ bytes?: number;
5
7
  }
6
8
  export interface ModelHandleOptions {
7
- load(): Promise<ModelResource>;
9
+ load(signal: AbortSignal): Promise<ModelResource>;
8
10
  onLoaded?(): void;
9
11
  onFailed?(err: unknown): void;
10
12
  }
@@ -15,8 +17,13 @@ export declare class ModelHandle {
15
17
  private readonly _onFailed;
16
18
  private _tracked;
17
19
  private _loading;
20
+ private _resource;
21
+ lastUsed: number;
22
+ private _attempt;
18
23
  constructor({ load, onLoaded, onFailed }: ModelHandleOptions);
19
24
  load(): Promise<ModelResource>;
20
25
  get loading(): boolean;
26
+ abandon(): void;
21
27
  peek(): Promise<ModelResource> | null;
28
+ get resource(): ModelResource | null;
22
29
  }
@@ -1,3 +1,5 @@
1
+ import { AbortController } from 'bare-abort-controller';
2
+ import { AbortedError } from '../helpers/race-abort.js';
1
3
  import { retryMemo } from '../helpers/retry-memo.js';
2
4
  export class ModelHandle {
3
5
  _load;
@@ -6,8 +8,15 @@ export class ModelHandle {
6
8
  _onFailed;
7
9
  _tracked = null;
8
10
  _loading = false;
11
+ _resource = null;
12
+ lastUsed = 0;
13
+ _attempt = null;
9
14
  constructor({ load, onLoaded, onFailed }) {
10
- const memo = retryMemo(load);
15
+ const memo = retryMemo(() => {
16
+ const attempt = new AbortController();
17
+ this._attempt = attempt;
18
+ return load(attempt.signal);
19
+ });
11
20
  this._load = memo;
12
21
  this._peek = memo.peek;
13
22
  this._onLoaded = onLoaded;
@@ -25,15 +34,24 @@ export class ModelHandle {
25
34
  report();
26
35
  }
27
36
  };
28
- attempt.then(() => settle(() => this._onLoaded?.()), (err) => settle(() => this._onFailed?.(err)));
37
+ attempt.then((resource) => settle(() => {
38
+ this._resource = resource;
39
+ this._onLoaded?.();
40
+ }), (err) => settle(() => this._onFailed?.(err)));
29
41
  }
30
42
  return attempt;
31
43
  }
32
44
  get loading() {
33
45
  return this._loading;
34
46
  }
47
+ abandon() {
48
+ this._attempt?.abort(new AbortedError());
49
+ }
35
50
  peek() {
36
51
  return this._peek();
37
52
  }
53
+ get resource() {
54
+ return this._loading ? null : this._resource;
55
+ }
38
56
  }
39
57
  function noop() { }
@@ -1,5 +1,5 @@
1
1
  import type * as QvacSdk from '@qvac/sdk';
2
- import type { ModelAddon, ModelRegistrySdk } from './model-source.ts';
2
+ import { type ModelAddon, type ModelRegistrySdk } from './model-source.ts';
3
3
  import { type ModelTaskType } from './model-task.ts';
4
4
  export type ModelType = ModelAddon;
5
5
  export interface StaticCatalogEntry {
@@ -22,6 +22,7 @@ export interface ModelInfo {
22
22
  actualSize?: number;
23
23
  label?: string;
24
24
  taskType: ModelTaskType;
25
+ memoryBytes?: number | undefined;
25
26
  }
26
27
  export type CatalogRow = Omit<ModelInfo, 'isLoading'>;
27
28
  export interface ListModelsOptions {
@@ -54,4 +55,10 @@ export interface CustomModelRowsContext {
54
55
  }
55
56
  export declare function customModelRows({ type, cachedOnly }: ListModelsOptions, { modelsDir, loaded }: CustomModelRowsContext): Promise<CatalogRow[]>;
56
57
  export declare function mergeCustomRows(catalog: ReadonlyArray<CatalogRow>, custom: ReadonlyArray<CatalogRow>): CatalogRow[];
58
+ export interface ModelSourceQuery {
59
+ readonly model: string;
60
+ readonly addon: ModelAddon;
61
+ readonly modelsDir: string | undefined;
62
+ }
63
+ export declare function isModelSourceCached(sdk: ModelListSdk, { model, addon, modelsDir }: ModelSourceQuery): Promise<boolean>;
57
64
  export {};
@@ -1,3 +1,4 @@
1
+ import { resolveModelSrc } from './model-source.js';
1
2
  import { addonTaskType } from './model-task.js';
2
3
  import { isCustomModelCached } from './custom-model-cache.js';
3
4
  import { companionRegistryPaths, curatedListables, customModels, musicListables } from './model-catalog.js';
@@ -153,3 +154,25 @@ export function mergeCustomRows(catalog, custom) {
153
154
  const shadowed = new Set(custom.map((row) => row.name));
154
155
  return [...catalog.filter((row) => !shadowed.has(row.name)), ...custom];
155
156
  }
157
+ export async function isModelSourceCached(sdk, { model, addon, modelsDir }) {
158
+ const src = await resolveModelSrc(sdk, model, addon).catch(() => null);
159
+ if (src === null) {
160
+ return false;
161
+ }
162
+ const name = registryCatalogName(sdk, src);
163
+ if (name === undefined) {
164
+ return typeof src === 'string' && isCustomModelCached({ name: model, src }, modelsDir);
165
+ }
166
+ const info = await sdk.getModelInfo({ name }).catch(() => null);
167
+ return info?.isCached ?? false;
168
+ }
169
+ function registryCatalogName(sdk, src) {
170
+ if (typeof src !== 'string') {
171
+ return sdk.models.find((entry) => entry.registryPath === src.registryPath)?.name ?? src.name;
172
+ }
173
+ const registryPath = /^registry:\/\/[^/]+\/(.+)$/.exec(src)?.[1];
174
+ if (registryPath === undefined) {
175
+ return undefined;
176
+ }
177
+ return sdk.models.find((entry) => entry.registryPath === registryPath)?.name;
178
+ }
@@ -3,14 +3,29 @@ import { ModelHandle } from './model-handle.ts';
3
3
  import { type ModelSrc } from './context.ts';
4
4
  import { type ModelAddon } from './model-source.ts';
5
5
  import type { GpuFaultState } from './sdk-faults.ts';
6
+ import type { HoldFn } from './download-gate.ts';
6
7
  type SdkModule = typeof import('@qvac/sdk');
8
+ type FitModelType = NonNullable<import('@qvac/sdk').AssessModelFitInput['models'][number]['modelType']>;
7
9
  export interface ModelState {
8
- status: 'loading' | 'downloading' | 'downloaded' | 'ready' | 'error' | 'unloading' | 'unloaded';
10
+ status: 'held' | 'loading' | 'downloading' | 'downloaded' | 'ready' | 'error' | 'unloading' | 'unloaded';
9
11
  model: string;
10
12
  catalogChanged?: boolean;
11
13
  [key: string]: unknown;
12
14
  }
13
15
  export type NotifyFn = (state: ModelState) => void;
16
+ export interface PendingLoad {
17
+ readonly key: string;
18
+ readonly model: string;
19
+ readonly modelSrc: ModelSrc;
20
+ readonly modelType: FitModelType;
21
+ readonly modelConfig?: Record<string, unknown> | undefined;
22
+ }
23
+ export interface Admission {
24
+ readonly bytes: number | undefined;
25
+ release(): void;
26
+ }
27
+ export type BeforeLoad = (load: PendingLoad) => Promise<Admission | undefined>;
28
+ export type ChooseContext = (load: PendingLoad, tier: number) => Promise<number>;
14
29
  export interface LlmDeviceConfig {
15
30
  device?: 'cpu' | 'gpu';
16
31
  gpuLayers?: number;
@@ -80,6 +95,13 @@ export declare function modelOf(key: string): string;
80
95
  export declare function releaseModel(pool: PoolLike, key: string): void;
81
96
  export declare function loadedModels(pool: PoolLike): LoadedModel[];
82
97
  export declare function settledModels(pool: PoolLike): string[];
98
+ export interface SettledEntry {
99
+ readonly key: string;
100
+ readonly refs: number;
101
+ readonly bytes: number | undefined;
102
+ readonly lastUsed: number;
103
+ }
104
+ export declare function settledEntries(pool: PoolLike): SettledEntry[];
83
105
  export declare function loadingModels(pool: PoolLike): string[];
84
106
  export interface ResidentLease {
85
107
  model: string;
@@ -104,6 +126,7 @@ export interface ModelCapacityOptions extends DeadlineOptions {
104
126
  }
105
127
  export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal, evictable }: ModelCapacityOptions): Promise<boolean>;
106
128
  export declare function evictModelWithin(pool: PoolLike, key: string, options: DeadlineOptions): Promise<boolean>;
129
+ export declare function evictIdleWithin(pool: PoolLike, key: string, { deadlineMs }: DeadlineOptions): Promise<boolean>;
107
130
  export declare function evictModel(pool: PoolLike, model: string, options: DeadlineOptions): Promise<boolean>;
108
131
  export type LoadSdk = () => Promise<ModelLifecycleSdk>;
109
132
  export type EmbedLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySearch'> & Partial<CancelSdk> & {
@@ -134,15 +157,20 @@ export declare function loadClockGuard(model: string, sdk: Partial<CancelSdk>, {
134
157
  export interface ModelPoolOptions {
135
158
  deviceConfig?: LlmDeviceConfig;
136
159
  loads?: LoadTracker;
160
+ beforeLoad?: BeforeLoad;
161
+ chooseContext?: ChooseContext;
162
+ hold?: HoldFn;
137
163
  }
138
- export declare function createModelPool(loadSdk: LoadSdk, notify: NotifyFn, { deviceConfig, loads }?: ModelPoolOptions): ModelPuddle;
164
+ export declare function createModelPool(loadSdk: LoadSdk, notify: NotifyFn, { deviceConfig, loads, beforeLoad, chooseContext, hold }?: ModelPoolOptions): ModelPuddle;
139
165
  export interface EmbedPoolOptions {
140
166
  faults: GpuFaultState;
141
167
  batchSize?: number;
142
168
  openclCacheDir?: (() => string | undefined) | undefined;
143
169
  loads?: LoadTracker;
170
+ beforeLoad?: BeforeLoad;
171
+ hold?: HoldFn;
144
172
  }
145
- export declare function createEmbedPool(loadSdk: LoadEmbedSdk, notify: NotifyFn, { faults, batchSize, openclCacheDir, loads }: EmbedPoolOptions): ModelPuddle;
173
+ export declare function createEmbedPool(loadSdk: LoadEmbedSdk, notify: NotifyFn, { faults, batchSize, openclCacheDir, loads, beforeLoad, hold }: EmbedPoolOptions): ModelPuddle;
146
174
  export type VoiceLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySearch'> & Partial<CancelSdk> & {
147
175
  loadModel(params: {
148
176
  modelSrc: ModelSrc;
@@ -153,12 +181,14 @@ export type VoiceLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySe
153
181
  };
154
182
  export type LoadVoiceSdk = () => Promise<VoiceLifecycleSdk>;
155
183
  export interface VoicePoolOptions {
156
- modelType: string;
184
+ modelType: FitModelType;
157
185
  addon: ModelAddon;
158
186
  modelConfig?: Record<string, unknown> | undefined;
159
187
  modelConfigFor?: (modelSrc: ModelSrc) => Readonly<Record<string, unknown>> | undefined;
160
188
  openclCacheDir?: (() => string | undefined) | undefined;
161
189
  loads?: LoadTracker;
190
+ beforeLoad?: BeforeLoad;
191
+ hold?: HoldFn;
162
192
  }
163
- export declare function createVoicePool(loadSdk: LoadVoiceSdk, notify: NotifyFn, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads }: VoicePoolOptions): ModelPuddle;
193
+ export declare function createVoicePool(loadSdk: LoadVoiceSdk, notify: NotifyFn, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads, beforeLoad, hold }: VoicePoolOptions): ModelPuddle;
164
194
  export {};
@@ -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,12 +357,12 @@ 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, chooseContext, 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);
326
- const ctxSize = resolved.ctxSize || defaultContextSize(modelSrc, customContextLength(model));
365
+ const tier = defaultContextSize(modelSrc, customContextLength(model));
327
366
  const projectionModelSrc = resolved.projectionModel
328
367
  ? await resolveModelSrc(sdk, resolved.projectionModel)
329
368
  : undefined;
@@ -337,11 +376,10 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
337
376
  const openclCache = deviceConfig.device !== 'cpu' && deviceConfig.openclCacheDir
338
377
  ? deviceConfig.openclCacheDir()
339
378
  : undefined;
340
- const modelConfig = definedFields({
379
+ const config = definedFields({
341
380
  verbosity: llmVerbosity(),
342
381
  tools: true,
343
382
  stop_sequences: RUNAWAY_STOP_SEQUENCES,
344
- ctx_size: ctxSize,
345
383
  'cache-type-k': kvCache.k,
346
384
  'cache-type-v': kvCache.v,
347
385
  projectionModelSrc,
@@ -350,56 +388,80 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
350
388
  'main-gpu': mainGpu,
351
389
  openclCacheDir: openclCache || undefined
352
390
  });
391
+ const pending = { key, model, modelSrc, modelType: 'llamacpp-completion' };
392
+ const ctxSize = resolved.ctxSize ||
393
+ (chooseContext ? await chooseContext({ ...pending, modelConfig: config }, tier) : tier);
394
+ const modelConfig = { ...config, ctx_size: ctxSize };
353
395
  const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
354
396
  hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
397
+ const admission = await beforeLoad?.({ ...pending, modelConfig });
355
398
  const loadStarted = Date.now();
356
- const modelId = await loadClockGuard(model, sdk).race(loads.track(sdk.loadModel({
399
+ const modelId = await loadClockGuard(model, sdk)
400
+ .race(loads.track(sdk.loadModel({
357
401
  modelSrc,
358
402
  modelType: 'llamacpp-completion',
359
403
  modelConfig,
360
404
  onProgress: (progress) => notify(progressState(model, progress))
361
- })));
405
+ })))
406
+ .finally(() => admission?.release());
362
407
  hlog('llm loaded', { model, loadMs: Date.now() - loadStarted });
363
- return { model, modelId, ctxSize };
408
+ return { model, modelId, ctxSize, bytes: admission?.bytes };
364
409
  };
365
410
  return new ModelPuddle({
366
- open: (key, model = key, opts = {}) => reportedHandle(notify, model, () => load(model, opts)),
411
+ open: (key, model = key, opts = {}) => reportedHandle({ notify, model, addon: 'llm', hold, load: () => load(key, model, opts) }),
367
412
  gc: unloadOnGc(loadSdk, notify),
368
413
  ...NO_IDLE_GC
369
414
  });
370
415
  }
371
- export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_EMBED_BATCH_SIZE, openclCacheDir, loads = createLoadTracker() }) {
372
- const load = async (model) => {
416
+ export function createEmbedPool(loadSdk, notify, { faults, batchSize = DEFAULT_EMBED_BATCH_SIZE, openclCacheDir, loads = createLoadTracker(), beforeLoad, hold }) {
417
+ const load = async (key, model) => {
373
418
  const sdk = await loadSdk();
374
419
  const modelSrc = await resolveModelSrc(sdk, model, 'embeddings');
375
420
  const openclCache = faults.device !== 'cpu' && openclCacheDir ? openclCacheDir() : undefined;
376
- const modelId = await loadClockGuard(model, sdk).race(loads.track(sdk.loadModel({
421
+ const modelConfig = {
422
+ batchSize,
423
+ gpuLayers: faults.gpuLayers(),
424
+ device: faults.device,
425
+ openclCacheDir: openclCache || undefined
426
+ };
427
+ const admission = await beforeLoad?.({
428
+ key,
429
+ model,
430
+ modelSrc,
431
+ modelType: 'llamacpp-embedding',
432
+ modelConfig
433
+ });
434
+ const modelId = await loadClockGuard(model, sdk)
435
+ .race(loads.track(sdk.loadModel({
377
436
  modelSrc,
378
437
  modelType: 'llamacpp-embedding',
379
- modelConfig: {
380
- batchSize,
381
- gpuLayers: faults.gpuLayers(),
382
- device: faults.device,
383
- openclCacheDir: openclCache || undefined
384
- },
438
+ modelConfig,
385
439
  onProgress: (progress) => notify(progressState(model, progress))
386
- })));
387
- return { model, modelId };
440
+ })))
441
+ .finally(() => admission?.release());
442
+ return { model, modelId, bytes: admission?.bytes };
388
443
  };
389
444
  return new ModelPuddle({
390
- open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
445
+ open: (key, model = key) => reportedHandle({ notify, model, addon: 'embeddings', hold, load: () => load(key, model) }),
391
446
  gc: unloadOnGc(loadSdk, notify),
392
447
  ...NO_IDLE_GC
393
448
  });
394
449
  }
395
- export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads = createLoadTracker() }) {
450
+ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig, modelConfigFor, openclCacheDir, loads = createLoadTracker(), beforeLoad, hold }) {
396
451
  const wantsGpu = modelConfig?.['useGPU'] === true;
397
452
  let gpuRefused = false;
398
- const load = async (model) => {
453
+ const load = async (key, model) => {
399
454
  const sdk = await loadSdk();
400
455
  const modelSrc = await resolveModelSrc(sdk, model, addon);
401
456
  const tuned = modelConfigFor?.(modelSrc);
402
457
  const loadConfig = tuned ? { ...modelConfig, ...tuned } : modelConfig;
458
+ const admission = await beforeLoad?.({
459
+ key,
460
+ model,
461
+ modelSrc,
462
+ modelType,
463
+ modelConfig: loadConfig
464
+ });
403
465
  const attempt = () => {
404
466
  const config = gpuRefused ? { ...loadConfig, useGPU: false } : loadConfig;
405
467
  const openclCache = gpuRefused ? undefined : openclCacheDir?.();
@@ -410,17 +472,19 @@ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig
410
472
  onProgress: (progress) => notify(progressState(model, progress))
411
473
  }));
412
474
  };
413
- const modelId = await loadClockGuard(model, sdk).race(attempt().catch((err) => {
475
+ const modelId = await loadClockGuard(model, sdk)
476
+ .race(attempt().catch((err) => {
414
477
  if (!wantsGpu || gpuRefused || loads.draining)
415
478
  throw err;
416
479
  hlog('voice gpu load refused, retrying on cpu', { model, modelType, error: String(err) });
417
480
  gpuRefused = true;
418
481
  return attempt();
419
- }));
420
- return { model, modelId };
482
+ }))
483
+ .finally(() => admission?.release());
484
+ return { model, modelId, bytes: admission?.bytes };
421
485
  };
422
486
  return new ModelPuddle({
423
- open: (key, model = key) => reportedHandle(notify, model, () => load(model)),
487
+ open: (key, model = key) => reportedHandle({ notify, model, addon, hold, load: () => load(key, model) }),
424
488
  gc: unloadOnGc(loadSdk, notify),
425
489
  ...NO_IDLE_GC
426
490
  });
@@ -445,7 +509,8 @@ function unloadOnGc(loadSdk, notify) {
445
509
  try {
446
510
  await sdk.unloadModel({ modelId });
447
511
  }
448
- catch {
512
+ catch (err) {
513
+ hlog('unload failed', { model, err: errorMessage(err) });
449
514
  continue;
450
515
  }
451
516
  notify({ status: 'unloaded', model, catalogChanged: true });
@@ -1,6 +1,7 @@
1
1
  import type { EmbedDevice } from './embed-config.ts';
2
2
  export declare function isSdkGpuDeviceLostError(err: unknown): boolean;
3
3
  export declare function isSdkModelWedgedError(err: unknown): boolean;
4
+ export declare function isGrammarOverrunError(err: unknown): boolean;
4
5
  export declare function isContextOverflowError(err: unknown): boolean;
5
6
  export type GpuFaultDecision = 'reload' | 'fallback';
6
7
  interface GpuFaultEscalation {
@@ -16,6 +16,12 @@ export function isSdkModelWedgedError(err) {
16
16
  }
17
17
  return /failed to (decode next token|eval chunk)/i.test(message);
18
18
  }
19
+ export function isGrammarOverrunError(err) {
20
+ if (typeof err !== 'object' || err === null)
21
+ return false;
22
+ const message = 'message' in err ? err.message : undefined;
23
+ return typeof message === 'string' && /empty grammar stack/i.test(message);
24
+ }
19
25
  export function isContextOverflowError(err) {
20
26
  if (typeof err !== 'object' || err === null)
21
27
  return false;
@@ -0,0 +1,23 @@
1
+ import type { Box } from './inherit.ts';
2
+ import type { SlideSize } from './template.ts';
3
+ export type Role = 'slot' | 'blank' | 'body' | 'picture' | 'other';
4
+ export interface Size {
5
+ readonly width: number;
6
+ readonly height: number;
7
+ }
8
+ export interface Placed {
9
+ readonly box: Box;
10
+ readonly role: Role;
11
+ }
12
+ export interface Beside<T> {
13
+ readonly shape: T;
14
+ readonly box: Box;
15
+ }
16
+ export interface Area<T> {
17
+ readonly box: Box;
18
+ readonly cleared: readonly T[];
19
+ readonly beside: Beside<T> | null;
20
+ }
21
+ export declare function framed(slide: SlideSize): Box;
22
+ export declare function fitted(size: Size, area: Box): Box;
23
+ export declare function pictureArea<T extends Placed>(slide: SlideSize, shapes: readonly T[]): Area<T>;