@qvac/harness 0.3.3 → 0.5.0

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 (301) hide show
  1. package/CHANGELOG.md +700 -126
  2. package/README.md +156 -0
  3. package/dist/harness.bundle +71 -103
  4. package/dist/prebuilds-manifest.json +0 -8
  5. package/dist-lib/index.d.ts +42 -40
  6. package/dist-lib/index.js +307 -117
  7. package/dist-lib/lib/audio-transcode-bare.d.ts +2 -2
  8. package/dist-lib/lib/audio-transcode-bare.js +28 -18
  9. package/dist-lib/lib/audio-transcode-node.d.ts +2 -2
  10. package/dist-lib/lib/capabilities/audio-resample.d.ts +7 -6
  11. package/dist-lib/lib/capabilities/audio-resample.js +30 -18
  12. package/dist-lib/lib/capabilities/completion.d.ts +20 -19
  13. package/dist-lib/lib/capabilities/completion.js +194 -135
  14. package/dist-lib/lib/capabilities/download.d.ts +6 -6
  15. package/dist-lib/lib/capabilities/download.js +15 -8
  16. package/dist-lib/lib/capabilities/embed.js +2 -1
  17. package/dist-lib/lib/capabilities/enhance.d.ts +6 -1
  18. package/dist-lib/lib/capabilities/enhance.js +7 -4
  19. package/dist-lib/lib/capabilities/invoke.js +7 -4
  20. package/dist-lib/lib/capabilities/kv-cache.js +2 -1
  21. package/dist-lib/lib/capabilities/preview-model-source.d.ts +69 -0
  22. package/dist-lib/lib/capabilities/preview-model-source.js +439 -0
  23. package/dist-lib/lib/capabilities/speech-text.js +5 -4
  24. package/dist-lib/lib/capabilities/tool-selection.js +10 -9
  25. package/dist-lib/lib/capabilities/tool-turns.js +30 -16
  26. package/dist-lib/lib/capabilities/usage-breakdown.js +2 -1
  27. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  28. package/dist-lib/lib/capabilities/voice.js +22 -12
  29. package/dist-lib/lib/connect.d.ts +15 -37
  30. package/dist-lib/lib/connect.js +161 -112
  31. package/dist-lib/lib/context-window.js +25 -21
  32. package/dist-lib/lib/contract-types.d.ts +26 -20
  33. package/dist-lib/lib/diagram-repair.d.ts +4 -4
  34. package/dist-lib/lib/diagram-repair.js +6 -3
  35. package/dist-lib/lib/docx/index.d.ts +3 -2
  36. package/dist-lib/lib/{error-fields.d.ts → guards.d.ts} +1 -0
  37. package/dist-lib/lib/{error-fields.js → guards.js} +3 -0
  38. package/dist-lib/lib/hardware/capability-probe.js +20 -10
  39. package/dist-lib/lib/hardware/gpu-classify.js +6 -3
  40. package/dist-lib/lib/hardware/gpu-probe-addon.js +8 -4
  41. package/dist-lib/lib/hardware/gpu-probe.js +4 -2
  42. package/dist-lib/lib/hardware/gpu-stats.native.cjs +8 -9
  43. package/dist-lib/lib/hardware/parsers.js +8 -4
  44. package/dist-lib/lib/helpers/buffer-burst.js +4 -2
  45. package/dist-lib/lib/helpers/control-tokens.js +4 -2
  46. package/dist-lib/lib/helpers/join-guard.d.ts +2 -2
  47. package/dist-lib/lib/helpers/join-guard.js +4 -2
  48. package/dist-lib/lib/helpers/load-stall.js +4 -2
  49. package/dist-lib/lib/helpers/markup-stream-filter.d.ts +2 -2
  50. package/dist-lib/lib/helpers/markup-stream-filter.js +6 -3
  51. package/dist-lib/lib/helpers/race-abort.js +2 -1
  52. package/dist-lib/lib/helpers/race-timeout.d.ts +5 -0
  53. package/dist-lib/lib/helpers/race-timeout.js +14 -0
  54. package/dist-lib/lib/helpers/run-outcome.d.ts +6 -8
  55. package/dist-lib/lib/helpers/run-outcome.js +36 -20
  56. package/dist-lib/lib/helpers/spawn-path.js +6 -3
  57. package/dist-lib/lib/helpers/text-coalescer.js +6 -3
  58. package/dist-lib/lib/helpers/thinking.js +38 -45
  59. package/dist-lib/lib/helpers/tool-call-markup.d.ts +9 -7
  60. package/dist-lib/lib/helpers/tool-call-markup.js +36 -20
  61. package/dist-lib/lib/helpers/tool-intent.d.ts +3 -2
  62. package/dist-lib/lib/helpers/tool-intent.js +4 -2
  63. package/dist-lib/lib/helpers/tool-label.js +18 -9
  64. package/dist-lib/lib/hooks/built-in/arg-repair.js +23 -13
  65. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  66. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  67. package/dist-lib/lib/hooks/pipeline.d.ts +5 -4
  68. package/dist-lib/lib/hooks/pipeline.js +6 -3
  69. package/dist-lib/lib/hooks/run.js +12 -8
  70. package/dist-lib/lib/model-download-progress.js +10 -8
  71. package/dist-lib/lib/models/context.d.ts +1 -1
  72. package/dist-lib/lib/models/context.js +5 -1
  73. package/dist-lib/lib/models/custom-model-cache.d.ts +13 -0
  74. package/dist-lib/lib/models/custom-model-cache.js +70 -0
  75. package/dist-lib/lib/models/gguf-header.d.ts +31 -0
  76. package/dist-lib/lib/models/gguf-header.js +230 -0
  77. package/dist-lib/lib/models/idle-unload.d.ts +14 -0
  78. package/dist-lib/lib/models/idle-unload.js +51 -0
  79. package/dist-lib/lib/models/kv-cache.d.ts +3 -2
  80. package/dist-lib/lib/models/kv-cache.js +4 -3
  81. package/dist-lib/lib/models/llm-presets.d.ts +2 -2
  82. package/dist-lib/lib/models/llm-presets.js +4 -2
  83. package/dist-lib/lib/models/model-catalog-constants.js +3 -2
  84. package/dist-lib/lib/models/model-catalog.d.ts +19 -21
  85. package/dist-lib/lib/models/model-catalog.js +66 -33
  86. package/dist-lib/lib/models/model-delete.d.ts +4 -1
  87. package/dist-lib/lib/models/model-delete.js +30 -15
  88. package/dist-lib/lib/models/model-handle.js +2 -1
  89. package/dist-lib/lib/models/model-list.d.ts +14 -3
  90. package/dist-lib/lib/models/model-list.js +33 -6
  91. package/dist-lib/lib/models/model-pool.d.ts +44 -10
  92. package/dist-lib/lib/models/model-pool.js +47 -42
  93. package/dist-lib/lib/models/model-source.js +11 -6
  94. package/dist-lib/lib/models/music-catalog.d.ts +2 -2
  95. package/dist-lib/lib/models/nmt-catalog.d.ts +4 -3
  96. package/dist-lib/lib/models/nmt-catalog.js +10 -5
  97. package/dist-lib/lib/models/opencl-cache.d.ts +6 -1
  98. package/dist-lib/lib/models/opencl-cache.js +5 -3
  99. package/dist-lib/lib/models/sdk-faults.d.ts +11 -8
  100. package/dist-lib/lib/models/sdk-faults.js +9 -6
  101. package/dist-lib/lib/models/shards.d.ts +11 -0
  102. package/dist-lib/lib/models/shards.js +15 -0
  103. package/dist-lib/lib/models/tool-dialect.js +10 -5
  104. package/dist-lib/lib/no-redirect-fetch-bare.d.ts +3 -3
  105. package/dist-lib/lib/no-redirect-fetch-bare.js +1 -1
  106. package/dist-lib/lib/no-redirect-fetch-impl.js +6 -3
  107. package/dist-lib/lib/no-redirect-fetch-node.d.ts +1 -1
  108. package/dist-lib/lib/no-redirect-fetch-node.js +9 -9
  109. package/dist-lib/lib/office/index.d.ts +3 -2
  110. package/dist-lib/lib/office/python.d.ts +3 -2
  111. package/dist-lib/lib/office/python.js +2 -1
  112. package/dist-lib/lib/prompt.d.ts +5 -4
  113. package/dist-lib/lib/prompt.js +8 -4
  114. package/dist-lib/lib/queue.js +10 -5
  115. package/dist-lib/lib/resolution-detector.js +21 -11
  116. package/dist-lib/lib/rpc-session.js +2 -1
  117. package/dist-lib/lib/sdk-loader.js +2 -1
  118. package/dist-lib/lib/sdk-storage.js +4 -2
  119. package/dist-lib/lib/serve.js +128 -68
  120. package/dist-lib/lib/spawn.js +7 -4
  121. package/dist-lib/lib/tmp-path.d.ts +7 -6
  122. package/dist-lib/lib/tool-call-display.d.ts +2 -2
  123. package/dist-lib/lib/tools/create-documents.js +18 -10
  124. package/dist-lib/lib/tools/edit-documents.js +23 -13
  125. package/dist-lib/lib/tools/edit-image.d.ts +2 -2
  126. package/dist-lib/lib/tools/edit-image.js +60 -36
  127. package/dist-lib/lib/tools/exec/adapters/adapter.d.ts +10 -10
  128. package/dist-lib/lib/tools/exec/adapters/python/attachments.d.ts +17 -11
  129. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +53 -40
  130. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +2 -2
  131. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +60 -32
  132. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.d.ts +33 -22
  133. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +58 -29
  134. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.d.ts +2 -2
  135. package/dist-lib/lib/tools/exec/adapters/python/loader.js +23 -12
  136. package/dist-lib/lib/tools/exec/adapters/python/memfs.d.ts +13 -11
  137. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +10 -5
  138. package/dist-lib/lib/tools/exec/adapters/python/net-shim.js +3 -1
  139. package/dist-lib/lib/tools/exec/adapters/python/output-policy.d.ts +6 -4
  140. package/dist-lib/lib/tools/exec/adapters/python/output-policy.js +20 -10
  141. package/dist-lib/lib/tools/exec/adapters/python/pyodide.d.ts +2 -2
  142. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +14 -7
  143. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +41 -34
  144. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +316 -166
  145. package/dist-lib/lib/tools/exec/adapters/python/run-session.d.ts +9 -8
  146. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +56 -28
  147. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +8 -10
  148. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +22 -11
  149. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +3 -2
  150. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +41 -26
  151. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +27 -18
  152. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -2
  153. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.js +14 -13
  154. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.d.ts +4 -4
  155. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.js +10 -5
  156. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.d.ts +4 -4
  157. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +2 -1
  158. package/dist-lib/lib/tools/exec/obsidian/cli-process.d.ts +6 -1
  159. package/dist-lib/lib/tools/exec/obsidian/cli-process.js +8 -5
  160. package/dist-lib/lib/tools/exec/obsidian/cli.d.ts +6 -6
  161. package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
  162. package/dist-lib/lib/tools/exec/obsidian/command.d.ts +3 -2
  163. package/dist-lib/lib/tools/exec/obsidian/command.js +6 -3
  164. package/dist-lib/lib/tools/exec/obsidian/output.js +2 -1
  165. package/dist-lib/lib/tools/exec/obsidian/route.d.ts +4 -6
  166. package/dist-lib/lib/tools/exec/obsidian/route.js +17 -11
  167. package/dist-lib/lib/tools/exec/obsidian/safe-path.js +2 -1
  168. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +15 -7
  169. package/dist-lib/lib/tools/exec/obsidian/vault-paths.d.ts +2 -2
  170. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +20 -10
  171. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +20 -13
  172. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +24 -12
  173. package/dist-lib/lib/tools/exec/runner.d.ts +14 -14
  174. package/dist-lib/lib/tools/exec/runner.js +5 -3
  175. package/dist-lib/lib/tools/exec/scope.js +52 -28
  176. package/dist-lib/lib/tools/exec/shell-argv.js +31 -18
  177. package/dist-lib/lib/tools/exec/shell-guidance.d.ts +6 -1
  178. package/dist-lib/lib/tools/exec/shell-guidance.js +35 -20
  179. package/dist-lib/lib/tools/exec/video-tools.js +2 -2
  180. package/dist-lib/lib/tools/exec.d.ts +6 -8
  181. package/dist-lib/lib/tools/exec.js +113 -54
  182. package/dist-lib/lib/tools/file-glob.d.ts +4 -5
  183. package/dist-lib/lib/tools/file-glob.js +16 -11
  184. package/dist-lib/lib/tools/fs/glob.js +13 -18
  185. package/dist-lib/lib/tools/fs/remote-protocol.d.ts +10 -10
  186. package/dist-lib/lib/tools/fs/remote.d.ts +3 -2
  187. package/dist-lib/lib/tools/fs/remote.js +13 -7
  188. package/dist-lib/lib/tools/fs/scope.d.ts +8 -3
  189. package/dist-lib/lib/tools/fs/scope.js +20 -12
  190. package/dist-lib/lib/tools/fs/walk.js +6 -3
  191. package/dist-lib/lib/tools/fs/write.d.ts +3 -2
  192. package/dist-lib/lib/tools/generate-image.d.ts +11 -9
  193. package/dist-lib/lib/tools/generate-image.js +73 -37
  194. package/dist-lib/lib/tools/generate-music.d.ts +22 -11
  195. package/dist-lib/lib/tools/generate-music.js +79 -48
  196. package/dist-lib/lib/tools/grep.js +66 -41
  197. package/dist-lib/lib/tools/http-request/encode-fields.js +18 -9
  198. package/dist-lib/lib/tools/http-request/{utils.js → request.js} +20 -11
  199. package/dist-lib/lib/tools/http-request/truncate-response.js +57 -46
  200. package/dist-lib/lib/tools/http-request.d.ts +1 -1
  201. package/dist-lib/lib/tools/http-request.js +33 -18
  202. package/dist-lib/lib/tools/mcp/protocol.d.ts +53 -0
  203. package/dist-lib/lib/tools/mcp/{utils.js → protocol.js} +45 -23
  204. package/dist-lib/lib/tools/mcp-call.d.ts +1 -1
  205. package/dist-lib/lib/tools/mcp-call.js +37 -16
  206. package/dist-lib/lib/tools/operations/builders.js +10 -6
  207. package/dist-lib/lib/tools/operations/compile.js +20 -10
  208. package/dist-lib/lib/tools/operations/decoders.js +12 -6
  209. package/dist-lib/lib/tools/operations/gmail-message.js +3 -2
  210. package/dist-lib/lib/tools/outbound-guard.d.ts +4 -3
  211. package/dist-lib/lib/tools/outbound-guard.js +14 -8
  212. package/dist-lib/lib/tools/private-ip.js +8 -7
  213. package/dist-lib/lib/tools/rag-search.js +14 -7
  214. package/dist-lib/lib/tools/read-files.js +23 -20
  215. package/dist-lib/lib/tools/skill.js +19 -10
  216. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +11 -18
  217. package/dist-lib/lib/tools/skills/auth-resolver.js +30 -18
  218. package/dist-lib/lib/tools/skills/bin-check.d.ts +3 -2
  219. package/dist-lib/lib/tools/skills/bin-check.js +10 -3
  220. package/dist-lib/lib/tools/skills/bundled-dir.js +20 -10
  221. package/dist-lib/lib/tools/skills/bundled-hash.d.ts +3 -2
  222. package/dist-lib/lib/tools/skills/bundled-hash.js +6 -1
  223. package/dist-lib/lib/tools/skills/catalog.d.ts +3 -2
  224. package/dist-lib/lib/tools/skills/catalog.js +33 -28
  225. package/dist-lib/lib/tools/skills/cli-argv.d.ts +3 -2
  226. package/dist-lib/lib/tools/skills/cli-argv.js +55 -52
  227. package/dist-lib/lib/tools/skills/cli-schema-check.js +21 -10
  228. package/dist-lib/lib/tools/skills/cli-schema-load.js +28 -20
  229. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +6 -6
  230. package/dist-lib/lib/tools/skills/cli-schema.js +2 -1
  231. package/dist-lib/lib/tools/skills/credential-check.d.ts +6 -3
  232. package/dist-lib/lib/tools/skills/credential-check.js +24 -12
  233. package/dist-lib/lib/tools/skills/credential-grants.js +1 -6
  234. package/dist-lib/lib/tools/skills/credential-health.d.ts +5 -1
  235. package/dist-lib/lib/tools/skills/credential-health.js +5 -3
  236. package/dist-lib/lib/tools/skills/frontmatter-parser.js +10 -5
  237. package/dist-lib/lib/tools/skills/manifest-parse.js +84 -64
  238. package/dist-lib/lib/tools/skills/mention.js +7 -6
  239. package/dist-lib/lib/tools/skills/obsidian-readiness.d.ts +2 -2
  240. package/dist-lib/lib/tools/skills/obsidian-readiness.js +11 -3
  241. package/dist-lib/lib/tools/skills/overlay-crud.d.ts +6 -4
  242. package/dist-lib/lib/tools/skills/overlay-crud.js +12 -6
  243. package/dist-lib/lib/tools/skills/overlay-dir.js +4 -2
  244. package/dist-lib/lib/tools/skills/overlay-watch.d.ts +3 -2
  245. package/dist-lib/lib/tools/skills/overlay-watch.js +8 -4
  246. package/dist-lib/lib/tools/skills/setup-routes.js +1 -3
  247. package/dist-lib/lib/tools/skills/skill-settings.js +4 -1
  248. package/dist-lib/lib/tools/skills/skill-state.d.ts +6 -11
  249. package/dist-lib/lib/tools/skills/skill-state.js +48 -29
  250. package/dist-lib/lib/tools/skills/tool-grants.js +6 -3
  251. package/dist-lib/lib/tools/toolsets.js +2 -1
  252. package/dist-lib/lib/tools/translate.d.ts +4 -5
  253. package/dist-lib/lib/tools/translate.js +31 -14
  254. package/dist-lib/lib/tools/url-guard.js +2 -1
  255. package/dist-lib/lib/tools/weather-lookup/place.js +8 -4
  256. package/dist-lib/lib/tools/weather-lookup/report.d.ts +7 -1
  257. package/dist-lib/lib/tools/weather-lookup/report.js +46 -9
  258. package/dist-lib/lib/tools/weather-lookup.js +18 -12
  259. package/dist-lib/lib/tools/web-fetch.js +47 -24
  260. package/dist-lib/lib/tools/web-search.js +8 -3
  261. package/dist-lib/lib/validating-lookup.d.ts +4 -4
  262. package/dist-lib/lib/wav-encode.d.ts +1 -1
  263. package/dist-lib/lib/wav-encode.js +11 -10
  264. package/dist-lib/lib/web-fetch/embedded-state.js +24 -12
  265. package/dist-lib/lib/web-fetch/extract.js +4 -2
  266. package/dist-lib/lib/web-fetch/images.js +8 -4
  267. package/dist-lib/lib/web-fetch/video.d.ts +2 -2
  268. package/dist-lib/lib/web-fetch/video.js +8 -4
  269. package/dist-lib/lib/web-search/engine-registry.d.ts +2 -2
  270. package/dist-lib/lib/web-search/engines/aol.js +1 -1
  271. package/dist-lib/lib/web-search/engines/bing.js +1 -1
  272. package/dist-lib/lib/web-search/engines/brave.js +4 -1
  273. package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -2
  274. package/dist-lib/lib/web-search/engines/google.js +7 -3
  275. package/dist-lib/lib/web-search/engines/wikipedia.js +5 -4
  276. package/dist-lib/lib/web-search/engines/yahoo.js +1 -1
  277. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -4
  278. package/dist-lib/lib/web-search/fetch-html.js +13 -7
  279. package/dist-lib/lib/web-search/orchestrator.d.ts +9 -5
  280. package/dist-lib/lib/web-search/orchestrator.js +3 -3
  281. package/dist-lib/lib/wire.d.ts +3 -2
  282. package/dist-lib/lib/wire.js +23 -15
  283. package/dist-lib/schema/baseline.js +10 -5
  284. package/dist-lib/schema/build.js +2 -1
  285. package/dist-lib/schema/harness.d.ts +15 -2
  286. package/dist-lib/schema/harness.js +55 -2
  287. package/dist-lib/schema/release-tags.d.ts +3 -2
  288. package/dist-lib/schema/release-tags.js +6 -3
  289. package/dist-lib/spec/bind.js +6 -0
  290. package/dist-lib/spec/calls.js +2 -0
  291. package/dist-lib/spec/hrpc/hrpc.json +26 -0
  292. package/dist-lib/spec/hrpc/index.js +27 -3
  293. package/dist-lib/spec/hrpc/messages.js +207 -8
  294. package/dist-lib/spec/hyperschema/index.js +207 -8
  295. package/dist-lib/spec/hyperschema/schema.json +165 -1
  296. package/dist-lib/spec/hyperschema/types.d.ts +42 -1
  297. package/package.json +13 -14
  298. package/worker/harness-sidecar.mjs +16 -22
  299. package/worker/stow-entry.mjs +7 -4
  300. package/dist-lib/lib/tools/mcp/utils.d.ts +0 -47
  301. /package/dist-lib/lib/tools/http-request/{utils.d.ts → request.d.ts} +0 -0
@@ -0,0 +1,51 @@
1
+ import { raceTimeout } from '../helpers/race-timeout.js';
2
+ const DEFAULT_TICK_MS = 60_000;
3
+ export function createIdleReaper({ sweep, waitMs, onError, now = Date.now, tickMs = DEFAULT_TICK_MS }) {
4
+ let timeoutMs = 0;
5
+ let inflight = 0;
6
+ let lastActivity = now();
7
+ let sweeping = null;
8
+ let timer = null;
9
+ const tick = () => {
10
+ if (sweeping !== null || inflight > 0 || now() - lastActivity < timeoutMs) {
11
+ return;
12
+ }
13
+ const run = sweep().catch(onError).catch(noop);
14
+ sweeping = raceTimeout(run, { ms: waitMs, fallback: undefined }).finally(() => {
15
+ sweeping = null;
16
+ lastActivity = now();
17
+ });
18
+ };
19
+ const disarm = () => {
20
+ if (timer !== null) {
21
+ clearInterval(timer);
22
+ }
23
+ timer = null;
24
+ };
25
+ return {
26
+ configure(ms) {
27
+ timeoutMs = ms;
28
+ disarm();
29
+ if (ms <= 0) {
30
+ return;
31
+ }
32
+ timer = setInterval(tick, Math.min(tickMs, ms));
33
+ if (typeof timer.unref === 'function') {
34
+ timer.unref();
35
+ }
36
+ },
37
+ async enter() {
38
+ inflight++;
39
+ lastActivity = now();
40
+ if (sweeping !== null) {
41
+ await sweeping;
42
+ }
43
+ },
44
+ exit() {
45
+ inflight--;
46
+ lastActivity = now();
47
+ },
48
+ close: disarm
49
+ };
50
+ }
51
+ function noop() { }
@@ -1,4 +1,5 @@
1
- export declare function adrenoSafeKvCache(device: 'cpu' | 'gpu' | undefined, requestedK: string | undefined, requestedV: string | undefined, hostPlatform?: string): {
1
+ export interface KvCacheTypes {
2
2
  k: string | undefined;
3
3
  v: string | undefined;
4
- };
4
+ }
5
+ export declare function adrenoSafeKvCache(device: 'cpu' | 'gpu' | undefined, { k, v }: KvCacheTypes, hostPlatform?: string): KvCacheTypes;
@@ -1,6 +1,7 @@
1
1
  import { platform } from 'os';
2
- export function adrenoSafeKvCache(device, requestedK, requestedV, hostPlatform = platform()) {
3
- if (device === 'cpu' || hostPlatform !== 'android')
4
- return { k: requestedK, v: requestedV };
2
+ export function adrenoSafeKvCache(device, { k, v }, hostPlatform = platform()) {
3
+ if (device === 'cpu' || hostPlatform !== 'android') {
4
+ return { k, v };
5
+ }
5
6
  return { k: 'f16', v: 'f16' };
6
7
  }
@@ -1,4 +1,4 @@
1
- export type LlmGenerationParams = {
1
+ export interface LlmGenerationParams {
2
2
  temp?: number;
3
3
  top_p?: number;
4
4
  top_k?: number;
@@ -6,5 +6,5 @@ export type LlmGenerationParams = {
6
6
  repeat_penalty?: number;
7
7
  frequency_penalty?: number;
8
8
  reasoning_budget: -1 | 0;
9
- };
9
+ }
10
10
  export declare function resolveLlmGenerationParams(modelName: string | undefined, reasoningEnabled: boolean | undefined, deviceDefault?: boolean): LlmGenerationParams | undefined;
@@ -31,11 +31,13 @@ export function resolveLlmGenerationParams(modelName, reasoningEnabled, deviceDe
31
31
  const resolver = modelName
32
32
  ? PRESET_RESOLVERS.find((entry) => modelName.startsWith(entry.prefix))
33
33
  : undefined;
34
- if (resolver)
34
+ if (resolver) {
35
35
  return resolver.resolve(reasoningEnabled);
36
+ }
36
37
  const effective = reasoningEnabled ?? deviceDefault;
37
- if (effective === undefined)
38
+ if (effective === undefined) {
38
39
  return undefined;
40
+ }
39
41
  return { reasoning_budget: effective ? -1 : 0 };
40
42
  }
41
43
  function qwen08BPreset(reasoningEnabled) {
@@ -14,7 +14,7 @@ export const QWEN_38_27B = 'Qwen3.8-27B-UD-Q4_K_XL';
14
14
  export const MEDPSY_1_7B = 'medpsy-1.7b-q4_k_m-imat';
15
15
  export const MEDPSY_4B = 'medpsy-4b-q4_k_m-imat';
16
16
  export function isCapabilityProfile(value) {
17
- return CAPABILITY_PROFILE_ORDER.includes(value);
17
+ return CAPABILITY_PROFILE_ORDER.some((profile) => profile === value);
18
18
  }
19
19
  export function meetsMinCapabilityProfile(active, required) {
20
20
  return CAPABILITY_PROFILE_ORDER.indexOf(active) >= CAPABILITY_PROFILE_ORDER.indexOf(required);
@@ -41,8 +41,9 @@ const PROFILE_TO_MODEL_NAME = new Map([
41
41
  ]);
42
42
  export function capabilityProfileToModelName(profile) {
43
43
  const name = PROFILE_TO_MODEL_NAME.get(profile);
44
- if (!name)
44
+ if (!name) {
45
45
  throw new Error(`no model for capability profile ${profile}`);
46
+ }
46
47
  return name;
47
48
  }
48
49
  export const AGENT_MODEL_NAMES = [
@@ -1,4 +1,6 @@
1
1
  import type { ModelRegistryEntry, ToolDialect } from '@qvac/sdk';
2
+ import type { ImageModelSet } from '../tools/generate-image.ts';
3
+ import type { MusicModelStages } from '../tools/generate-music.ts';
2
4
  import type { ModelAddon } from './model-source.ts';
3
5
  export interface SdkModelAsset extends ModelRegistryEntry {
4
6
  readonly src: string;
@@ -24,10 +26,11 @@ export interface CategoryDefault {
24
26
  export declare const CATEGORY_DEFAULTS: ReadonlyArray<CategoryDefault>;
25
27
  export declare function defaultVoiceModel(category: ModelCategory): string;
26
28
  export declare function defaultTtsFor(profile: CapabilityProfile): CategoryDefault;
27
- export declare function infraModelsFor(profile: CapabilityProfile): Array<{
28
- name: string;
29
- addon: ModelAddon;
30
- }>;
29
+ export interface CatalogModel {
30
+ readonly name: string;
31
+ readonly addon: ModelAddon;
32
+ }
33
+ export declare function infraModelsFor(profile: CapabilityProfile): CatalogModel[];
31
34
  export declare function mediaModelNameFor(category: ModelCategory, profile: CapabilityProfile): string;
32
35
  export interface DiffusionCatalogEntry {
33
36
  readonly choice: ImageModelChoice;
@@ -62,12 +65,7 @@ export declare const AUDIOGEN_CATALOG: ReadonlyArray<AudioGenCatalogEntry>;
62
65
  export declare function musicDefaultSteps(choice: string | undefined): number | undefined;
63
66
  export declare function musicChoiceForDit(dit: string): MusicModelChoice | undefined;
64
67
  export declare function musicSizeBytes(entry: AudioGenCatalogEntry): number;
65
- export declare function musicModelSet(choice: string | undefined): {
66
- dit: string;
67
- textEnc: string;
68
- lm: string;
69
- vae: string;
70
- } | null;
68
+ export declare function musicModelSet(choice: string | undefined): MusicModelStages | null;
71
69
  export interface AudioGenDownloadMetadata {
72
70
  readonly companions: ReadonlyArray<string>;
73
71
  readonly assetSizes: Readonly<Record<string, number>>;
@@ -80,17 +78,9 @@ export interface AgentSkillModelInput {
80
78
  readonly imageModelChoice?: ImageModelChoice | null;
81
79
  readonly musicModelChoice?: MusicModelChoice | null;
82
80
  }
83
- export declare function skillModelsForAgent(agent: AgentSkillModelInput): ReadonlyArray<{
84
- readonly name: string;
85
- readonly addon: ModelAddon;
86
- }>;
81
+ export declare function skillModelsForAgent(agent: AgentSkillModelInput): ReadonlyArray<CatalogModel>;
87
82
  export declare function diffusionSizeBytes(entry: DiffusionCatalogEntry): number;
88
- export declare function imageModelFor(choice: string | undefined): {
89
- model: string;
90
- llmModel?: string | undefined;
91
- vaeModel?: string | undefined;
92
- prediction?: 'flux2_flow' | undefined;
93
- } | null;
83
+ export declare function imageModelFor(choice: string | undefined): ImageModelSet | null;
94
84
  export interface ImageClamps {
95
85
  maxDim?: number;
96
86
  maxSteps?: number;
@@ -105,13 +95,21 @@ export interface CuratedListable {
105
95
  }
106
96
  export declare function curatedListables(): CuratedListable[];
107
97
  export declare function curatedModelAsset(name: string, addon: ModelRegistryEntry['addon']): SdkModelAsset | undefined;
98
+ export declare const CUSTOM_TASK_TYPES: readonly ['llm'];
99
+ export type CustomTaskType = (typeof CUSTOM_TASK_TYPES)[number];
108
100
  export interface CustomModelEntry {
109
101
  readonly name: string;
110
102
  readonly src: string;
111
103
  readonly projectionSrc?: string;
104
+ readonly taskType?: CustomTaskType;
105
+ readonly expectedSize?: number;
106
+ readonly label?: string;
107
+ readonly contextLength?: number;
112
108
  }
113
109
  export declare function setCustomModels(entries: ReadonlyArray<CustomModelEntry>): void;
114
- export declare function customModelSrc(name: string): string | undefined;
110
+ export declare function customModels(): ReadonlyArray<CustomModelEntry>;
111
+ export declare function customContextLength(name: string): number | undefined;
112
+ export declare function customModelSrc(name: string, addon: ModelRegistryEntry['addon']): string | undefined;
115
113
  export declare function customToolDialect(name: string): ToolDialect | undefined;
116
114
  export declare function customProjectionMap(): Record<string, string>;
117
115
  export declare function companionRegistryPaths(): Map<string, string[]>;
@@ -56,8 +56,9 @@ export const LLM_CATALOG = [
56
56
  export function defaultProjectionMap() {
57
57
  const map = {};
58
58
  for (const entry of LLM_CATALOG) {
59
- if (entry.projection)
59
+ if (entry.projection) {
60
60
  map[entry.name] = entry.projection.src;
61
+ }
61
62
  }
62
63
  return map;
63
64
  }
@@ -73,13 +74,18 @@ export const CATEGORY_DEFAULTS = [
73
74
  ];
74
75
  export function defaultVoiceModel(category) {
75
76
  const entry = CATEGORY_DEFAULTS.find((candidate) => candidate.category === category);
76
- if (!entry)
77
+ if (!entry) {
77
78
  throw new Error(`no default model for category ${category}`);
79
+ }
78
80
  return entry.asset.src;
79
81
  }
80
82
  export function defaultTtsFor(profile) {
81
83
  const name = defaultTtsModelName(profile);
82
- return CATEGORY_DEFAULTS.find((entry) => entry.name === name);
84
+ const entry = CATEGORY_DEFAULTS.find((candidate) => candidate.name === name);
85
+ if (!entry) {
86
+ throw new Error(`no default tts model named ${name}`);
87
+ }
88
+ return entry;
83
89
  }
84
90
  export function infraModelsFor(profile) {
85
91
  const tts = defaultTtsFor(profile);
@@ -87,11 +93,13 @@ export function infraModelsFor(profile) {
87
93
  return [tts, ...rest].map((entry) => ({ name: entry.name, addon: entry.asset.addon }));
88
94
  }
89
95
  export function mediaModelNameFor(category, profile) {
90
- if (category === 'tts')
96
+ if (category === 'tts') {
91
97
  return defaultTtsFor(profile).name;
98
+ }
92
99
  const entry = CATEGORY_DEFAULTS.find((candidate) => candidate.category === category);
93
- if (!entry)
100
+ if (!entry) {
94
101
  throw new Error(`no default model for category ${category}`);
102
+ }
95
103
  return entry.name;
96
104
  }
97
105
  const IMAGE_GENERATION_SKILL = 'image-generation';
@@ -123,10 +131,12 @@ export const DIFFUSION_CATALOG = [
123
131
  ];
124
132
  function diffusionAssets(entry) {
125
133
  const assets = [entry.model];
126
- if (entry.llmModel)
134
+ if (entry.llmModel) {
127
135
  assets.push(entry.llmModel);
128
- if (entry.vaeModel)
136
+ }
137
+ if (entry.vaeModel) {
129
138
  assets.push(entry.vaeModel);
139
+ }
130
140
  return assets;
131
141
  }
132
142
  function diffusionCatalogEntry(model) {
@@ -142,12 +152,14 @@ export function diffusionDownloadMetadata(model, addon) {
142
152
  if (addon !== 'diffusion')
143
153
  return undefined;
144
154
  const entry = diffusionCatalogEntry(model);
145
- if (!entry)
155
+ if (!entry) {
146
156
  return undefined;
157
+ }
147
158
  const assets = diffusionAssets(entry);
148
159
  const assetSizes = {};
149
- for (const asset of assets)
160
+ for (const asset of assets) {
150
161
  assetSizes[asset.src] = asset.expectedSize;
162
+ }
151
163
  return {
152
164
  companions: assets.slice(1).map((asset) => asset.src),
153
165
  assetSizes
@@ -202,8 +214,9 @@ export function musicSizeBytes(entry) {
202
214
  }
203
215
  export function musicModelSet(choice) {
204
216
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.choice === choice);
205
- if (!entry)
217
+ if (!entry) {
206
218
  return null;
219
+ }
207
220
  return {
208
221
  dit: entry.dit.src,
209
222
  textEnc: entry.textEnc.src,
@@ -215,12 +228,14 @@ export function audioGenDownloadMetadata(model, addon) {
215
228
  if (addon !== 'audiogen')
216
229
  return undefined;
217
230
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.name === model || candidate.dit.name === model || candidate.dit.src === model);
218
- if (!entry)
231
+ if (!entry) {
219
232
  return undefined;
233
+ }
220
234
  const stages = audioGenStages(entry);
221
235
  const assetSizes = {};
222
- for (const asset of stages)
236
+ for (const asset of stages) {
223
237
  assetSizes[asset.src] = asset.expectedSize;
238
+ }
224
239
  return {
225
240
  companions: stages.slice(1).map((asset) => asset.src),
226
241
  assetSizes
@@ -235,14 +250,16 @@ export function skillModelsForAgent(agent) {
235
250
  if (agentHasSkill(agent, IMAGE_GENERATION_SKILL)) {
236
251
  const choice = agent.imageModelChoice ?? DEFAULT_IMAGE_MODEL_CHOICE;
237
252
  const entry = DIFFUSION_CATALOG.find((candidate) => candidate.choice === choice);
238
- if (entry)
253
+ if (entry) {
239
254
  models.push({ name: entry.name, addon: entry.model.addon });
255
+ }
240
256
  }
241
257
  if (agentHasSkill(agent, MUSIC_GENERATION_SKILL)) {
242
258
  const choice = agent.musicModelChoice ?? DEFAULT_MUSIC_MODEL_CHOICE;
243
259
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.choice === choice);
244
- if (entry)
260
+ if (entry) {
245
261
  models.push({ name: entry.name, addon: entry.dit.addon });
262
+ }
246
263
  }
247
264
  return models;
248
265
  }
@@ -251,13 +268,14 @@ export function diffusionSizeBytes(entry) {
251
268
  }
252
269
  export function imageModelFor(choice) {
253
270
  const entry = DIFFUSION_CATALOG.find((candidate) => candidate.choice === choice);
254
- if (!entry)
271
+ if (!entry) {
255
272
  return null;
273
+ }
256
274
  return {
257
275
  model: entry.model.src,
258
- ...(entry.llmModel ? { llmModel: entry.llmModel.src } : {}),
259
- ...(entry.vaeModel ? { vaeModel: entry.vaeModel.src } : {}),
260
- ...(entry.prediction ? { prediction: entry.prediction } : {})
276
+ llmModel: entry.llmModel?.src,
277
+ vaeModel: entry.vaeModel?.src,
278
+ prediction: entry.prediction
261
279
  };
262
280
  }
263
281
  export function imageClampsFromEnv(env) {
@@ -267,9 +285,10 @@ export function imageClampsFromEnv(env) {
267
285
  };
268
286
  const maxDim = positive(env.QVAC_IMAGE_MAX_DIM);
269
287
  const maxSteps = positive(env.QVAC_IMAGE_MAX_STEPS);
270
- if (!maxDim && !maxSteps)
288
+ if (!maxDim && !maxSteps) {
271
289
  return undefined;
272
- return { ...(maxDim ? { maxDim } : {}), ...(maxSteps ? { maxSteps } : {}) };
290
+ }
291
+ return { maxDim, maxSteps };
273
292
  }
274
293
  export function imageModelFromEnv(env, onIgnoredCompanions, onIgnoredPrediction) {
275
294
  const catalog = imageModelFor(env.QVAC_IMAGE_MODEL);
@@ -279,12 +298,14 @@ export function imageModelFromEnv(env, onIgnoredCompanions, onIgnoredPrediction)
279
298
  }
280
299
  return catalog;
281
300
  }
282
- if (!env.QVAC_IMAGE_MODEL)
301
+ if (!env.QVAC_IMAGE_MODEL) {
283
302
  return undefined;
303
+ }
284
304
  const prediction = env.QVAC_IMAGE_PREDICTION || undefined;
285
305
  const fluxSet = Boolean(env.QVAC_IMAGE_LLM && env.QVAC_IMAGE_VAE);
286
- if (prediction && !fluxSet)
306
+ if (prediction && !fluxSet) {
287
307
  onIgnoredPrediction?.();
308
+ }
288
309
  return {
289
310
  model: env.QVAC_IMAGE_MODEL,
290
311
  llmModel: env.QVAC_IMAGE_LLM || undefined,
@@ -304,44 +325,55 @@ export function curatedListables() {
304
325
  ...DIFFUSION_CATALOG.map((entry) => ({
305
326
  name: entry.name,
306
327
  asset: entry.model,
307
- companions: [entry.llmModel, entry.vaeModel].filter(Boolean)
328
+ companions: [entry.llmModel, entry.vaeModel].filter((asset) => asset !== undefined)
308
329
  }))
309
330
  ];
310
331
  }
311
332
  export function curatedModelAsset(name, addon) {
312
333
  return curatedListables().find((entry) => entry.asset.addon === addon && (entry.name === name || entry.asset.name === name))?.asset;
313
334
  }
335
+ export const CUSTOM_TASK_TYPES = ['llm'];
314
336
  let CUSTOM_MODELS = new Map();
315
337
  export function setCustomModels(entries) {
316
338
  CUSTOM_MODELS = new Map(entries.map((entry) => [entry.name, entry]));
317
339
  }
318
- export function customModelSrc(name) {
319
- return CUSTOM_MODELS.get(name)?.src;
340
+ export function customModels() {
341
+ return [...CUSTOM_MODELS.values()];
342
+ }
343
+ export function customContextLength(name) {
344
+ return CUSTOM_MODELS.get(name)?.contextLength;
345
+ }
346
+ export function customModelSrc(name, addon) {
347
+ const entry = CUSTOM_MODELS.get(name);
348
+ return entry && (entry.taskType ?? 'llm') === addon ? entry.src : undefined;
320
349
  }
321
350
  export function customToolDialect(name) {
322
351
  const entry = CUSTOM_MODELS.get(name);
323
- return entry ? detectToolDialect(entry.src, entry.name) : undefined;
352
+ return entry
353
+ ? detectToolDialect(entry.src, entry.label?.replace(/\s+/g, '-') ?? entry.name)
354
+ : undefined;
324
355
  }
325
356
  export function customProjectionMap() {
326
357
  const map = {};
327
358
  for (const entry of CUSTOM_MODELS.values()) {
328
- if (entry.projectionSrc)
359
+ if (entry.projectionSrc) {
329
360
  map[entry.name] = entry.projectionSrc;
361
+ }
330
362
  }
331
363
  return map;
332
364
  }
333
365
  export function companionRegistryPaths() {
334
366
  const map = new Map();
335
367
  for (const entry of LLM_CATALOG) {
336
- if (entry.projection)
368
+ if (entry.projection) {
337
369
  map.set(entry.weight.registryPath, [entry.projection.registryPath]);
370
+ }
338
371
  }
339
372
  for (const entry of DIFFUSION_CATALOG) {
340
- const companions = [entry.llmModel, entry.vaeModel]
341
- .filter(Boolean)
342
- .map((asset) => asset.registryPath);
343
- if (companions.length)
373
+ const companions = [entry.llmModel, entry.vaeModel].flatMap((asset) => asset ? [asset.registryPath] : []);
374
+ if (companions.length) {
344
375
  map.set(entry.model.registryPath, companions);
376
+ }
345
377
  }
346
378
  for (const entry of AUDIOGEN_CATALOG) {
347
379
  map.set(entry.dit.registryPath, [
@@ -355,8 +387,9 @@ export function companionRegistryPaths() {
355
387
  export function audioGenModelByName(name) {
356
388
  for (const entry of AUDIOGEN_CATALOG) {
357
389
  for (const stage of [entry.dit, entry.textEnc, entry.lm, entry.vae]) {
358
- if (stage.name === name || entry.name === name)
390
+ if (stage.name === name || entry.name === name) {
359
391
  return name === entry.name ? entry.dit : stage;
392
+ }
360
393
  }
361
394
  }
362
395
  return undefined;
@@ -15,4 +15,7 @@ export interface DeleteModelSummary {
15
15
  readonly freedBytes: number;
16
16
  }
17
17
  export declare function summarizeDelete(result: DeleteModelResult): DeleteModelSummary;
18
- export declare function deleteModelFiles(sdk: ModelDeleteSdk, name: string): Promise<DeleteModelResult>;
18
+ export interface DeleteModelOptions {
19
+ readonly modelsDir?: string;
20
+ }
21
+ export declare function deleteModelFiles(sdk: ModelDeleteSdk, name: string, { modelsDir }?: DeleteModelOptions): Promise<DeleteModelResult>;
@@ -1,27 +1,40 @@
1
1
  import fs from 'fs';
2
- import { curatedListables, musicListables } from './model-catalog.js';
2
+ import { customDownloads } from './custom-model-cache.js';
3
+ import { curatedListables, customModels, musicListables } from './model-catalog.js';
3
4
  export function summarizeDelete(result) {
4
5
  return { name: result.name, removed: result.removed.length, freedBytes: result.freedBytes };
5
6
  }
6
- export async function deleteModelFiles(sdk, name) {
7
- const seen = new Set();
7
+ export async function deleteModelFiles(sdk, name, { modelsDir } = {}) {
8
+ const custom = customModels().find((entry) => entry.name === name);
9
+ const { files, dirs } = custom
10
+ ? await customDownloads(custom, modelsDir)
11
+ : { files: await catalogFiles(sdk, name), dirs: [] };
8
12
  const removed = [];
13
+ for (const filePath of new Set(files)) {
14
+ const bytes = await onDiskSize(filePath);
15
+ if (bytes === null) {
16
+ continue;
17
+ }
18
+ await fs.promises.rm(filePath, { force: true });
19
+ removed.push({ path: filePath, bytes });
20
+ }
21
+ for (const dir of dirs) {
22
+ await fs.promises.rm(dir, { recursive: true, force: true });
23
+ }
24
+ return { name, removed, freedBytes: removed.reduce((sum, file) => sum + file.bytes, 0) };
25
+ }
26
+ async function catalogFiles(sdk, name) {
27
+ const paths = [];
9
28
  for (const catalogName of namesToPurge(name)) {
10
29
  const info = await sdk.getModelInfo({ name: catalogName }).catch(() => null);
11
- if (!info)
30
+ if (!info) {
12
31
  continue;
32
+ }
13
33
  for (const file of info.cacheFiles) {
14
- if (seen.has(file.path))
15
- continue;
16
- seen.add(file.path);
17
- const bytes = await onDiskSize(file.path);
18
- if (bytes === null)
19
- continue;
20
- await fs.promises.rm(file.path, { force: true });
21
- removed.push({ path: file.path, bytes });
34
+ paths.push(file.path);
22
35
  }
23
36
  }
24
- return { name, removed, freedBytes: removed.reduce((sum, file) => sum + file.bytes, 0) };
37
+ return paths;
25
38
  }
26
39
  async function onDiskSize(filePath) {
27
40
  try {
@@ -36,12 +49,14 @@ function namesToPurge(name) {
36
49
  const curated = curatedListables().find((entry) => entry.name === name || entry.asset.name === name);
37
50
  if (curated) {
38
51
  const names = [curated.asset.name, ...curated.companions.map((companion) => companion.name)];
39
- if (curated.projection)
52
+ if (curated.projection) {
40
53
  names.push(curated.projection.name);
54
+ }
41
55
  return names;
42
56
  }
43
57
  const music = musicListables().find((entry) => entry.name === name || entry.asset.name === name);
44
- if (music)
58
+ if (music) {
45
59
  return [music.asset.name, ...music.companions.map((stage) => stage.name)];
60
+ }
46
61
  return [name];
47
62
  }
@@ -16,8 +16,9 @@ export class ModelHandle {
16
16
  this._tracked = attempt;
17
17
  this._loading = true;
18
18
  const settled = () => {
19
- if (this._tracked === attempt)
19
+ if (this._tracked === attempt) {
20
20
  this._loading = false;
21
+ }
21
22
  };
22
23
  attempt.then(settled, settled);
23
24
  }
@@ -18,6 +18,7 @@ export interface ModelInfo {
18
18
  supportsImages: boolean;
19
19
  expectedSize: number;
20
20
  actualSize?: number;
21
+ label?: string;
21
22
  }
22
23
  export interface ListModelsOptions {
23
24
  type?: ModelType;
@@ -31,12 +32,22 @@ interface ModelStatus {
31
32
  expectedSize?: number;
32
33
  actualSize?: number;
33
34
  }
34
- export type ModelStatusCache = {
35
+ export interface ModelStatusCache {
35
36
  readonly entries: Map<string, ModelStatus>;
36
37
  epoch: number;
37
- };
38
+ }
38
39
  export declare const MODEL_STATUS_TTL_MS = 30000;
39
40
  export declare function createModelStatusCache(): ModelStatusCache;
40
41
  export declare function invalidateModelStatusCache(cache: ModelStatusCache): void;
41
- export declare function listModels(sdk: ModelListSdk, { type, cachedOnly, fullRegistry }?: ListModelsOptions, cache?: ModelStatusCache, projectionFor?: (name: string) => string | undefined): Promise<ModelInfo[]>;
42
+ export interface ListModelsRuntime {
43
+ cache?: ModelStatusCache | undefined;
44
+ projectionFor?: ((name: string) => string | undefined) | undefined;
45
+ }
46
+ export declare function listModels(sdk: ModelListSdk, { type, cachedOnly, fullRegistry }?: ListModelsOptions, { cache, projectionFor }?: ListModelsRuntime): Promise<ModelInfo[]>;
47
+ export interface CustomModelRowsContext {
48
+ readonly modelsDir?: string;
49
+ readonly loaded: ReadonlySet<string>;
50
+ }
51
+ export declare function customModelRows({ type, cachedOnly }: ListModelsOptions, { modelsDir, loaded }: CustomModelRowsContext): Promise<ModelInfo[]>;
52
+ export declare function mergeCustomRows(catalog: ReadonlyArray<ModelInfo>, custom: ReadonlyArray<ModelInfo>): ModelInfo[];
42
53
  export {};
@@ -1,4 +1,5 @@
1
- import { companionRegistryPaths, curatedListables, musicListables } from './model-catalog.js';
1
+ import { isCustomModelCached } from './custom-model-cache.js';
2
+ import { companionRegistryPaths, curatedListables, customModels, musicListables } from './model-catalog.js';
2
3
  export const MODEL_STATUS_TTL_MS = 30_000;
3
4
  export function createModelStatusCache() {
4
5
  return { entries: new Map(), epoch: 0 };
@@ -14,8 +15,9 @@ async function mapPool(items, limit, fn) {
14
15
  const worker = async () => {
15
16
  while (true) {
16
17
  const i = cursor++;
17
- if (i >= items.length)
18
+ if (i >= items.length) {
18
19
  return;
20
+ }
19
21
  results[i] = await fn(items[i]);
20
22
  }
21
23
  };
@@ -45,13 +47,14 @@ async function musicRows(hydrate) {
45
47
  }
46
48
  return rows;
47
49
  }
48
- export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, cache, projectionFor) {
50
+ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, { cache, projectionFor } = {}) {
49
51
  const now = Date.now();
50
52
  const seesImages = (name, addon) => addon === 'llm' && Boolean(projectionFor?.(name));
51
53
  const hydrate = async (registryPath, catalogName) => {
52
54
  const hit = cache?.entries.get(registryPath);
53
- if (hit && now - hit.at < MODEL_STATUS_TTL_MS)
55
+ if (hit && now - hit.at < MODEL_STATUS_TTL_MS) {
54
56
  return hit;
57
+ }
55
58
  const epoch = cache?.epoch;
56
59
  const info = catalogName
57
60
  ? await sdk.getModelInfo({ name: catalogName }).catch(() => null)
@@ -63,8 +66,9 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, c
63
66
  expectedSize: info?.expectedSize,
64
67
  actualSize: info?.actualSize
65
68
  };
66
- if (cache && cache.epoch === epoch)
69
+ if (cache && cache.epoch === epoch) {
67
70
  cache.entries.set(registryPath, status);
71
+ }
68
72
  return status;
69
73
  };
70
74
  if (!fullRegistry) {
@@ -98,8 +102,9 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, c
98
102
  }
99
103
  const entries = await sdk.modelRegistrySearch(type ? { addon: type } : {});
100
104
  const catalog = new Map();
101
- for (const c of sdk.models)
105
+ for (const c of sdk.models) {
102
106
  catalog.set(c.registryPath, c.name);
107
+ }
103
108
  const rows = await mapPool(entries, STATUS_CONCURRENCY, async (entry) => {
104
109
  const status = await hydrate(entry.registryPath, catalog.get(entry.registryPath));
105
110
  const companions = COMPANIONS_BY_WEIGHT.get(entry.registryPath) ?? [];
@@ -121,3 +126,25 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, c
121
126
  });
122
127
  return cachedOnly ? rows.filter((row) => row.isCached) : rows;
123
128
  }
129
+ export async function customModelRows({ type, cachedOnly }, { modelsDir, loaded }) {
130
+ const entries = customModels().filter((entry) => !type || (entry.taskType ?? 'llm') === type);
131
+ const rows = await Promise.all(entries.map(async (entry) => {
132
+ const addon = entry.taskType ?? 'llm';
133
+ return {
134
+ name: entry.name,
135
+ type: addon,
136
+ registryPath: entry.src,
137
+ registrySource: 'custom',
138
+ isCached: await isCustomModelCached(entry, modelsDir),
139
+ isLoaded: loaded.has(entry.name),
140
+ supportsImages: addon === 'llm' && Boolean(entry.projectionSrc),
141
+ expectedSize: entry.expectedSize ?? 0,
142
+ label: entry.label
143
+ };
144
+ }));
145
+ return cachedOnly ? rows.filter((row) => row.isCached) : rows;
146
+ }
147
+ export function mergeCustomRows(catalog, custom) {
148
+ const shadowed = new Set(custom.map((row) => row.name));
149
+ return [...catalog.filter((row) => !shadowed.has(row.name)), ...custom];
150
+ }