@elizaos/plugin-local-inference 2.0.0-beta.1 → 2.0.11-beta.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 (676) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/package.json +81 -15
  4. package/src/actions/generate-media.d.ts +59 -0
  5. package/src/actions/generate-media.d.ts.map +1 -0
  6. package/src/actions/generate-media.ts +647 -0
  7. package/src/actions/identify-speaker.d.ts +23 -0
  8. package/src/actions/identify-speaker.d.ts.map +1 -0
  9. package/src/actions/identify-speaker.ts +171 -0
  10. package/src/adapters/capacitor-llama/__tests__/compat-behavior.test.ts +218 -0
  11. package/src/adapters/capacitor-llama/__tests__/index.test.ts +68 -0
  12. package/src/adapters/capacitor-llama/__tests__/structured-output.test.ts +215 -0
  13. package/src/adapters/capacitor-llama/__tests__/text-streaming.test.ts +174 -0
  14. package/src/adapters/capacitor-llama/environment.ts +71 -0
  15. package/src/adapters/capacitor-llama/index.browser.ts +83 -0
  16. package/src/adapters/capacitor-llama/index.ts +807 -0
  17. package/src/adapters/capacitor-llama/loader.ts +109 -0
  18. package/src/adapters/capacitor-llama/structured-output.ts +165 -0
  19. package/src/adapters/capacitor-llama/text-streaming.ts +227 -0
  20. package/src/adapters/capacitor-llama/types.ts +374 -0
  21. package/src/backends/apple-foundation.ts +127 -0
  22. package/src/index.d.ts +7 -0
  23. package/src/index.d.ts.map +1 -0
  24. package/src/index.ts +54 -0
  25. package/src/local-inference-routes.d.ts +38 -0
  26. package/src/local-inference-routes.d.ts.map +1 -0
  27. package/src/local-inference-routes.test.ts +344 -0
  28. package/src/local-inference-routes.ts +1543 -0
  29. package/src/provider.d.ts +21 -0
  30. package/src/provider.d.ts.map +1 -0
  31. package/src/provider.ts +1171 -0
  32. package/src/routes/compat-helpers.d.ts +18 -0
  33. package/src/routes/compat-helpers.d.ts.map +1 -0
  34. package/src/routes/compat-helpers.ts +274 -0
  35. package/src/routes/family-member-route.d.ts +62 -0
  36. package/src/routes/family-member-route.d.ts.map +1 -0
  37. package/src/routes/family-member-route.ts +353 -0
  38. package/src/routes/index.d.ts +19 -0
  39. package/src/routes/index.d.ts.map +1 -0
  40. package/src/routes/index.ts +60 -0
  41. package/src/routes/live-diarization-route.d.ts +26 -0
  42. package/src/routes/live-diarization-route.d.ts.map +1 -0
  43. package/src/routes/live-diarization-route.test.ts +213 -0
  44. package/src/routes/live-diarization-route.ts +122 -0
  45. package/src/routes/local-inference-asr-route.d.ts +4 -0
  46. package/src/routes/local-inference-asr-route.d.ts.map +1 -0
  47. package/src/routes/local-inference-asr-route.test.ts +190 -0
  48. package/src/routes/local-inference-asr-route.ts +213 -0
  49. package/src/routes/local-inference-compat-routes.d.ts +16 -0
  50. package/src/routes/local-inference-compat-routes.d.ts.map +1 -0
  51. package/src/routes/local-inference-compat-routes.test.ts +423 -0
  52. package/src/routes/local-inference-compat-routes.ts +782 -0
  53. package/src/routes/local-inference-tts-route.d.ts +7 -0
  54. package/src/routes/local-inference-tts-route.d.ts.map +1 -0
  55. package/src/routes/local-inference-tts-route.test.ts +179 -0
  56. package/src/routes/local-inference-tts-route.ts +230 -0
  57. package/src/routes/voice-first-run-routes.d.ts +62 -0
  58. package/src/routes/voice-first-run-routes.d.ts.map +1 -0
  59. package/src/routes/voice-first-run-routes.ts +524 -0
  60. package/src/routes/voice-models-routes.d.ts +62 -0
  61. package/src/routes/voice-models-routes.d.ts.map +1 -0
  62. package/src/routes/voice-models-routes.ts +554 -0
  63. package/src/routes/voice-profile-plugin-routes.d.ts +19 -0
  64. package/src/routes/voice-profile-plugin-routes.d.ts.map +1 -0
  65. package/src/routes/voice-profile-plugin-routes.ts +138 -0
  66. package/src/routes/voice-profiles-management-routes.d.ts +52 -0
  67. package/src/routes/voice-profiles-management-routes.d.ts.map +1 -0
  68. package/src/routes/voice-profiles-management-routes.ts +476 -0
  69. package/src/routes/voice-speaker-profile-routes.d.ts +57 -0
  70. package/src/routes/voice-speaker-profile-routes.d.ts.map +1 -0
  71. package/src/routes/voice-speaker-profile-routes.ts +199 -0
  72. package/src/runtime/aosp-llama-loader-selection.test.ts +80 -0
  73. package/src/runtime/capacitor-llama.d.ts +25 -0
  74. package/src/runtime/embedding-manager-support.d.ts +77 -0
  75. package/src/runtime/embedding-manager-support.d.ts.map +1 -0
  76. package/src/runtime/embedding-manager-support.ts +497 -0
  77. package/src/runtime/embedding-presets.d.ts +16 -0
  78. package/src/runtime/embedding-presets.d.ts.map +1 -0
  79. package/src/runtime/embedding-presets.ts +81 -0
  80. package/src/runtime/embedding-warmup-policy.d.ts +14 -0
  81. package/src/runtime/embedding-warmup-policy.d.ts.map +1 -0
  82. package/src/runtime/embedding-warmup-policy.test.ts +53 -0
  83. package/src/runtime/embedding-warmup-policy.ts +48 -0
  84. package/src/runtime/ensure-local-inference-handler.d.ts +53 -0
  85. package/src/runtime/ensure-local-inference-handler.d.ts.map +1 -0
  86. package/src/runtime/ensure-local-inference-handler.test.ts +528 -0
  87. package/src/runtime/ensure-local-inference-handler.ts +1398 -0
  88. package/src/runtime/index.d.ts +14 -0
  89. package/src/runtime/index.d.ts.map +1 -0
  90. package/src/runtime/index.ts +27 -0
  91. package/src/runtime/mobile-local-inference-gate.d.ts +31 -0
  92. package/src/runtime/mobile-local-inference-gate.d.ts.map +1 -0
  93. package/src/runtime/mobile-local-inference-gate.test.ts +69 -0
  94. package/src/runtime/mobile-local-inference-gate.ts +44 -0
  95. package/src/runtime/voice-entity-binding.d.ts +103 -0
  96. package/src/runtime/voice-entity-binding.d.ts.map +1 -0
  97. package/src/runtime/voice-entity-binding.transcript.test.ts +69 -0
  98. package/src/runtime/voice-entity-binding.ts +328 -0
  99. package/src/services/README.md +71 -0
  100. package/src/services/__tests__/backend-selector.test.ts +101 -0
  101. package/src/services/__tests__/checkpoint-manager.test.ts +376 -0
  102. package/src/services/__tests__/gpu-autotune.test.ts +400 -0
  103. package/src/services/__tests__/llm-streaming-binding.test.ts +85 -0
  104. package/src/services/__tests__/planner-grammar.test.ts +372 -0
  105. package/src/services/__tests__/runtime-target.test.ts +176 -0
  106. package/src/services/active-model-switch-rollback.test.ts +183 -0
  107. package/src/services/active-model.d.ts +282 -0
  108. package/src/services/active-model.d.ts.map +1 -0
  109. package/src/services/active-model.ts +1213 -0
  110. package/src/services/asr/errors.d.ts +21 -0
  111. package/src/services/asr/errors.d.ts.map +1 -0
  112. package/src/services/asr/errors.ts +50 -0
  113. package/src/services/asr/hash.d.ts +28 -0
  114. package/src/services/asr/hash.d.ts.map +1 -0
  115. package/src/services/asr/hash.ts +49 -0
  116. package/src/services/asr/index.d.ts +76 -0
  117. package/src/services/asr/index.d.ts.map +1 -0
  118. package/src/services/asr/index.ts +178 -0
  119. package/src/services/asr/types.d.ts +91 -0
  120. package/src/services/asr/types.d.ts.map +1 -0
  121. package/src/services/asr/types.ts +95 -0
  122. package/src/services/assignments.d.ts +71 -0
  123. package/src/services/assignments.d.ts.map +1 -0
  124. package/src/services/assignments.test.ts +80 -0
  125. package/src/services/assignments.ts +230 -0
  126. package/src/services/backend-selector.ts +95 -0
  127. package/src/services/backend.d.ts +346 -0
  128. package/src/services/backend.d.ts.map +1 -0
  129. package/src/services/backend.ts +612 -0
  130. package/src/services/bundled-models.d.ts +34 -0
  131. package/src/services/bundled-models.d.ts.map +1 -0
  132. package/src/services/bundled-models.ts +129 -0
  133. package/src/services/cache-bridge.d.ts +206 -0
  134. package/src/services/cache-bridge.d.ts.map +1 -0
  135. package/src/services/cache-bridge.test.ts +516 -0
  136. package/src/services/cache-bridge.ts +423 -0
  137. package/src/services/catalog.d.ts +10 -0
  138. package/src/services/catalog.d.ts.map +1 -0
  139. package/src/services/catalog.test.ts +240 -0
  140. package/src/services/catalog.ts +27 -0
  141. package/src/services/checkpoint-client.d.ts +109 -0
  142. package/src/services/checkpoint-client.d.ts.map +1 -0
  143. package/src/services/checkpoint-client.ts +258 -0
  144. package/src/services/checkpoint-manager.ts +474 -0
  145. package/src/services/cloud-fallback.d.ts +102 -0
  146. package/src/services/cloud-fallback.d.ts.map +1 -0
  147. package/src/services/cloud-fallback.ts +230 -0
  148. package/src/services/conversation-registry.d.ts +142 -0
  149. package/src/services/conversation-registry.d.ts.map +1 -0
  150. package/src/services/conversation-registry.test.ts +235 -0
  151. package/src/services/conversation-registry.ts +264 -0
  152. package/src/services/desktop-fused-ffi-backend-runtime.d.ts +92 -0
  153. package/src/services/desktop-fused-ffi-backend-runtime.d.ts.map +1 -0
  154. package/src/services/desktop-fused-ffi-backend-runtime.ts +333 -0
  155. package/src/services/device-bridge.d.ts +188 -0
  156. package/src/services/device-bridge.d.ts.map +1 -0
  157. package/src/services/device-bridge.ts +1237 -0
  158. package/src/services/device-resource-metrics.d.ts +149 -0
  159. package/src/services/device-resource-metrics.d.ts.map +1 -0
  160. package/src/services/device-resource-metrics.test.ts +98 -0
  161. package/src/services/device-resource-metrics.ts +346 -0
  162. package/src/services/device-tier.d.ts +115 -0
  163. package/src/services/device-tier.d.ts.map +1 -0
  164. package/src/services/device-tier.test.ts +371 -0
  165. package/src/services/device-tier.ts +410 -0
  166. package/src/services/downloader.d.ts +82 -0
  167. package/src/services/downloader.d.ts.map +1 -0
  168. package/src/services/downloader.test.ts +724 -0
  169. package/src/services/downloader.ts +899 -0
  170. package/src/services/engine-direct-bundle.test.ts +58 -0
  171. package/src/services/engine-streaming.test.ts +80 -0
  172. package/src/services/engine.d.ts +534 -0
  173. package/src/services/engine.d.ts.map +1 -0
  174. package/src/services/engine.ts +1891 -0
  175. package/src/services/ensure-local-artifacts.integration.test.ts +273 -0
  176. package/src/services/ensure-local-artifacts.test.ts +368 -0
  177. package/src/services/ensure-local-artifacts.ts +351 -0
  178. package/src/services/external-scanner.d.ts +17 -0
  179. package/src/services/external-scanner.d.ts.map +1 -0
  180. package/src/services/external-scanner.ts +312 -0
  181. package/src/services/ffi-llm-mock.ts +354 -0
  182. package/src/services/ffi-llm-streaming-abi.ts +442 -0
  183. package/src/services/ffi-streaming-backend.d.ts +180 -0
  184. package/src/services/ffi-streaming-backend.d.ts.map +1 -0
  185. package/src/services/ffi-streaming-backend.ts +382 -0
  186. package/src/services/ffi-streaming-runner.d.ts +122 -0
  187. package/src/services/ffi-streaming-runner.d.ts.map +1 -0
  188. package/src/services/ffi-streaming-runner.test.ts +60 -0
  189. package/src/services/ffi-streaming-runner.ts +354 -0
  190. package/src/services/ffi-unload-ordering.test.ts +162 -0
  191. package/src/services/gpu-autotune.ts +534 -0
  192. package/src/services/gpu-detect.ts +139 -0
  193. package/src/services/handler-registry.d.ts +72 -0
  194. package/src/services/handler-registry.d.ts.map +1 -0
  195. package/src/services/handler-registry.ts +240 -0
  196. package/src/services/hardware.d.ts +63 -0
  197. package/src/services/hardware.d.ts.map +1 -0
  198. package/src/services/hardware.test.ts +183 -0
  199. package/src/services/hardware.ts +404 -0
  200. package/src/services/hf-search.d.ts +26 -0
  201. package/src/services/hf-search.d.ts.map +1 -0
  202. package/src/services/hf-search.test.ts +69 -0
  203. package/src/services/hf-search.ts +420 -0
  204. package/src/services/image-description-runtime.d.ts +14 -0
  205. package/src/services/image-description-runtime.d.ts.map +1 -0
  206. package/src/services/image-description-runtime.test.ts +61 -0
  207. package/src/services/image-description-runtime.ts +118 -0
  208. package/src/services/imagegen/aosp-unavailable.d.ts +134 -0
  209. package/src/services/imagegen/aosp-unavailable.d.ts.map +1 -0
  210. package/src/services/imagegen/aosp-unavailable.ts +229 -0
  211. package/src/services/imagegen/backend-selector.d.ts +118 -0
  212. package/src/services/imagegen/backend-selector.d.ts.map +1 -0
  213. package/src/services/imagegen/backend-selector.ts +281 -0
  214. package/src/services/imagegen/coreml-unavailable.d.ts +105 -0
  215. package/src/services/imagegen/coreml-unavailable.d.ts.map +1 -0
  216. package/src/services/imagegen/coreml-unavailable.ts +237 -0
  217. package/src/services/imagegen/errors.d.ts +16 -0
  218. package/src/services/imagegen/errors.d.ts.map +1 -0
  219. package/src/services/imagegen/errors.ts +40 -0
  220. package/src/services/imagegen/index.d.ts +58 -0
  221. package/src/services/imagegen/index.d.ts.map +1 -0
  222. package/src/services/imagegen/index.ts +144 -0
  223. package/src/services/imagegen/mflux.d.ts +74 -0
  224. package/src/services/imagegen/mflux.d.ts.map +1 -0
  225. package/src/services/imagegen/mflux.ts +313 -0
  226. package/src/services/imagegen/sd-cpp.d.ts +180 -0
  227. package/src/services/imagegen/sd-cpp.d.ts.map +1 -0
  228. package/src/services/imagegen/sd-cpp.ts +718 -0
  229. package/src/services/imagegen/tensorrt-unavailable.d.ts +83 -0
  230. package/src/services/imagegen/tensorrt-unavailable.d.ts.map +1 -0
  231. package/src/services/imagegen/tensorrt-unavailable.ts +295 -0
  232. package/src/services/imagegen/types.d.ts +181 -0
  233. package/src/services/imagegen/types.d.ts.map +1 -0
  234. package/src/services/imagegen/types.ts +193 -0
  235. package/src/services/index.d.ts +30 -0
  236. package/src/services/index.d.ts.map +1 -0
  237. package/src/services/index.ts +225 -0
  238. package/src/services/inference-capabilities.d.ts +132 -0
  239. package/src/services/inference-capabilities.d.ts.map +1 -0
  240. package/src/services/inference-capabilities.test.ts +75 -0
  241. package/src/services/inference-capabilities.ts +204 -0
  242. package/src/services/inference-telemetry.d.ts +59 -0
  243. package/src/services/inference-telemetry.d.ts.map +1 -0
  244. package/src/services/inference-telemetry.ts +143 -0
  245. package/src/services/ios-llama-streaming.ts +248 -0
  246. package/src/services/kv-spill.d.ts +189 -0
  247. package/src/services/kv-spill.d.ts.map +1 -0
  248. package/src/services/kv-spill.test.ts +222 -0
  249. package/src/services/kv-spill.ts +356 -0
  250. package/src/services/latency-trace.d.ts +346 -0
  251. package/src/services/latency-trace.d.ts.map +1 -0
  252. package/src/services/latency-trace.test.ts +266 -0
  253. package/src/services/latency-trace.ts +844 -0
  254. package/src/services/llama-server-metrics.ts +304 -0
  255. package/src/services/llm-streaming-binding.d.ts +96 -0
  256. package/src/services/llm-streaming-binding.d.ts.map +1 -0
  257. package/src/services/llm-streaming-binding.ts +136 -0
  258. package/src/services/load-args.d.ts +82 -0
  259. package/src/services/load-args.d.ts.map +1 -0
  260. package/src/services/load-args.ts +81 -0
  261. package/src/services/manifest/eliza-1.manifest.v1.json +708 -0
  262. package/src/services/manifest/index.d.ts +4 -0
  263. package/src/services/manifest/index.d.ts.map +1 -0
  264. package/src/services/manifest/index.ts +66 -0
  265. package/src/services/manifest/manifest.test.ts +693 -0
  266. package/src/services/manifest/schema.d.ts +715 -0
  267. package/src/services/manifest/schema.d.ts.map +1 -0
  268. package/src/services/manifest/schema.ts +655 -0
  269. package/src/services/manifest/types.d.ts +30 -0
  270. package/src/services/manifest/types.d.ts.map +1 -0
  271. package/src/services/manifest/types.ts +55 -0
  272. package/src/services/manifest/validator.d.ts +66 -0
  273. package/src/services/manifest/validator.d.ts.map +1 -0
  274. package/src/services/manifest/validator.ts +569 -0
  275. package/src/services/memory-arbiter.d.ts +343 -0
  276. package/src/services/memory-arbiter.d.ts.map +1 -0
  277. package/src/services/memory-arbiter.test.ts +419 -0
  278. package/src/services/memory-arbiter.ts +1000 -0
  279. package/src/services/memory-monitor.d.ts +119 -0
  280. package/src/services/memory-monitor.d.ts.map +1 -0
  281. package/src/services/memory-monitor.test.ts +208 -0
  282. package/src/services/memory-monitor.ts +296 -0
  283. package/src/services/memory-pressure.d.ts +127 -0
  284. package/src/services/memory-pressure.d.ts.map +1 -0
  285. package/src/services/memory-pressure.ts +413 -0
  286. package/src/services/mtp-doctor.d.ts +13 -0
  287. package/src/services/mtp-doctor.d.ts.map +1 -0
  288. package/src/services/mtp-doctor.ts +78 -0
  289. package/src/services/network-policy.d.ts +127 -0
  290. package/src/services/network-policy.d.ts.map +1 -0
  291. package/src/services/network-policy.ts +346 -0
  292. package/src/services/paths.d.ts +6 -0
  293. package/src/services/paths.d.ts.map +1 -0
  294. package/src/services/paths.ts +25 -0
  295. package/src/services/planner-skeleton.d.ts +124 -0
  296. package/src/services/planner-skeleton.d.ts.map +1 -0
  297. package/src/services/planner-skeleton.ts +175 -0
  298. package/src/services/providers.d.ts +38 -0
  299. package/src/services/providers.d.ts.map +1 -0
  300. package/src/services/providers.ts +507 -0
  301. package/src/services/ram-budget-cache.test.ts +163 -0
  302. package/src/services/ram-budget.d.ts +110 -0
  303. package/src/services/ram-budget.d.ts.map +1 -0
  304. package/src/services/ram-budget.ts +0 -0
  305. package/src/services/readiness.d.ts +9 -0
  306. package/src/services/readiness.d.ts.map +1 -0
  307. package/src/services/readiness.test.ts +87 -0
  308. package/src/services/readiness.ts +238 -0
  309. package/src/services/recommendation.d.ts +111 -0
  310. package/src/services/recommendation.d.ts.map +1 -0
  311. package/src/services/recommendation.ts +672 -0
  312. package/src/services/registry.d.ts +35 -0
  313. package/src/services/registry.d.ts.map +1 -0
  314. package/src/services/registry.ts +151 -0
  315. package/src/services/router-handler.d.ts +92 -0
  316. package/src/services/router-handler.d.ts.map +1 -0
  317. package/src/services/router-handler.test.ts +45 -0
  318. package/src/services/router-handler.ts +376 -0
  319. package/src/services/routing-policy.d.ts +55 -0
  320. package/src/services/routing-policy.d.ts.map +1 -0
  321. package/src/services/routing-policy.ts +228 -0
  322. package/src/services/routing-preferences.d.ts +8 -0
  323. package/src/services/routing-preferences.d.ts.map +1 -0
  324. package/src/services/routing-preferences.ts +15 -0
  325. package/src/services/runtime-target.d.ts +98 -0
  326. package/src/services/runtime-target.d.ts.map +1 -0
  327. package/src/services/runtime-target.ts +154 -0
  328. package/src/services/service.d.ts +128 -0
  329. package/src/services/service.d.ts.map +1 -0
  330. package/src/services/service.test.ts +223 -0
  331. package/src/services/service.ts +735 -0
  332. package/src/services/session-pool.d.ts +72 -0
  333. package/src/services/session-pool.d.ts.map +1 -0
  334. package/src/services/session-pool.ts +153 -0
  335. package/src/services/structured-output/deterministic-repair.d.ts +23 -0
  336. package/src/services/structured-output/deterministic-repair.d.ts.map +1 -0
  337. package/src/services/structured-output/deterministic-repair.test.ts +169 -0
  338. package/src/services/structured-output/deterministic-repair.ts +443 -0
  339. package/src/services/structured-output/index.ts +4 -0
  340. package/src/services/structured-output.d.ts +311 -0
  341. package/src/services/structured-output.d.ts.map +1 -0
  342. package/src/services/structured-output.test.ts +483 -0
  343. package/src/services/structured-output.ts +712 -0
  344. package/src/services/transcription-priority.test.ts +211 -0
  345. package/src/services/tts/errors.ts +46 -0
  346. package/src/services/tts/index.ts +214 -0
  347. package/src/services/tts/tts-audio-cache.ts +235 -0
  348. package/src/services/tts/types.ts +157 -0
  349. package/src/services/types.d.ts +19 -0
  350. package/src/services/types.d.ts.map +1 -0
  351. package/src/services/types.ts +55 -0
  352. package/src/services/verify-on-device.d.ts +34 -0
  353. package/src/services/verify-on-device.d.ts.map +1 -0
  354. package/src/services/verify-on-device.test.ts +87 -0
  355. package/src/services/verify-on-device.ts +127 -0
  356. package/src/services/verify.d.ts +8 -0
  357. package/src/services/verify.d.ts.map +1 -0
  358. package/src/services/verify.ts +13 -0
  359. package/src/services/vision/aosp-unavailable.d.ts +115 -0
  360. package/src/services/vision/aosp-unavailable.d.ts.map +1 -0
  361. package/src/services/vision/aosp-unavailable.ts +163 -0
  362. package/src/services/vision/capacitor-llama.d.ts +99 -0
  363. package/src/services/vision/capacitor-llama.d.ts.map +1 -0
  364. package/src/services/vision/capacitor-llama.ts +255 -0
  365. package/src/services/vision/cloud-fallback.d.ts +47 -0
  366. package/src/services/vision/cloud-fallback.d.ts.map +1 -0
  367. package/src/services/vision/cloud-fallback.test.ts +243 -0
  368. package/src/services/vision/cloud-fallback.ts +268 -0
  369. package/src/services/vision/fallback-chain.test.ts +86 -0
  370. package/src/services/vision/hash.d.ts +71 -0
  371. package/src/services/vision/hash.d.ts.map +1 -0
  372. package/src/services/vision/hash.ts +157 -0
  373. package/src/services/vision/index.d.ts +95 -0
  374. package/src/services/vision/index.d.ts.map +1 -0
  375. package/src/services/vision/index.ts +251 -0
  376. package/src/services/vision/llama-server.d.ts +73 -0
  377. package/src/services/vision/llama-server.d.ts.map +1 -0
  378. package/src/services/vision/llama-server.ts +177 -0
  379. package/src/services/vision/types.d.ts +153 -0
  380. package/src/services/vision/types.d.ts.map +1 -0
  381. package/src/services/vision/types.ts +154 -0
  382. package/src/services/vision/vast-fallback.d.ts +18 -0
  383. package/src/services/vision/vast-fallback.d.ts.map +1 -0
  384. package/src/services/vision/vast-fallback.ts +127 -0
  385. package/src/services/vision-embedding-cache.d.ts +98 -0
  386. package/src/services/vision-embedding-cache.d.ts.map +1 -0
  387. package/src/services/vision-embedding-cache.ts +189 -0
  388. package/src/services/voice/VOICE_WORKBENCH.md +88 -0
  389. package/src/services/voice/__test-helpers__/fake-ffi.ts +92 -0
  390. package/src/services/voice/__test-helpers__/synthetic-speech.ts +124 -0
  391. package/src/services/voice/__tests__/checkpoint-manager.test.ts +241 -0
  392. package/src/services/voice/__tests__/checkpoint-policy.test.ts +270 -0
  393. package/src/services/voice/__tests__/eager-context-builder.test.ts +257 -0
  394. package/src/services/voice/__tests__/eliza1-eot-scorer.test.ts +288 -0
  395. package/src/services/voice/__tests__/eot-classifier.test.ts +431 -0
  396. package/src/services/voice/__tests__/optimistic-rollback.test.ts +312 -0
  397. package/src/services/voice/__tests__/prefill-client.test.ts +266 -0
  398. package/src/services/voice/__tests__/prefix-preserving-queue.test.ts +208 -0
  399. package/src/services/voice/__tests__/streaming-asr.test.ts +450 -0
  400. package/src/services/voice/__tests__/streaming-transcriber.test.ts +339 -0
  401. package/src/services/voice/__tests__/turn-detector-resolver.test.ts +197 -0
  402. package/src/services/voice/__tests__/voice-state-machine-prefill.test.ts +275 -0
  403. package/src/services/voice/__tests__/voice-state-machine.test.ts +354 -0
  404. package/src/services/voice/audio-frame-consumer.d.ts +212 -0
  405. package/src/services/voice/audio-frame-consumer.d.ts.map +1 -0
  406. package/src/services/voice/audio-frame-consumer.test.ts +343 -0
  407. package/src/services/voice/audio-frame-consumer.ts +491 -0
  408. package/src/services/voice/barge-in.d.ts +112 -0
  409. package/src/services/voice/barge-in.d.ts.map +1 -0
  410. package/src/services/voice/barge-in.test.ts +244 -0
  411. package/src/services/voice/barge-in.ts +336 -0
  412. package/src/services/voice/cancellation-coordinator.d.ts +127 -0
  413. package/src/services/voice/cancellation-coordinator.d.ts.map +1 -0
  414. package/src/services/voice/cancellation-coordinator.test.ts +196 -0
  415. package/src/services/voice/cancellation-coordinator.ts +269 -0
  416. package/src/services/voice/checkpoint-manager.d.ts +199 -0
  417. package/src/services/voice/checkpoint-manager.d.ts.map +1 -0
  418. package/src/services/voice/checkpoint-manager.ts +401 -0
  419. package/src/services/voice/checkpoint-policy.ts +336 -0
  420. package/src/services/voice/composite-eot-classifier.test.ts +59 -0
  421. package/src/services/voice/e2e-harness.test.ts +182 -0
  422. package/src/services/voice/e2e-harness.ts +743 -0
  423. package/src/services/voice/eager-context-builder.d.ts +170 -0
  424. package/src/services/voice/eager-context-builder.d.ts.map +1 -0
  425. package/src/services/voice/eager-context-builder.ts +262 -0
  426. package/src/services/voice/eliza1-eot-scorer.d.ts +124 -0
  427. package/src/services/voice/eliza1-eot-scorer.d.ts.map +1 -0
  428. package/src/services/voice/eliza1-eot-scorer.ts +242 -0
  429. package/src/services/voice/embedding-server.ts +200 -0
  430. package/src/services/voice/embedding.d.ts +133 -0
  431. package/src/services/voice/embedding.d.ts.map +1 -0
  432. package/src/services/voice/embedding.test.ts +148 -0
  433. package/src/services/voice/embedding.ts +244 -0
  434. package/src/services/voice/emotion-attribution.d.ts +68 -0
  435. package/src/services/voice/emotion-attribution.d.ts.map +1 -0
  436. package/src/services/voice/emotion-attribution.test.ts +129 -0
  437. package/src/services/voice/emotion-attribution.ts +361 -0
  438. package/src/services/voice/engine-bridge-cancellation.test.ts +422 -0
  439. package/src/services/voice/engine-bridge.d.ts +746 -0
  440. package/src/services/voice/engine-bridge.d.ts.map +1 -0
  441. package/src/services/voice/engine-bridge.test.ts +384 -0
  442. package/src/services/voice/engine-bridge.ts +2226 -0
  443. package/src/services/voice/eot-classifier-ggml.d.ts +179 -0
  444. package/src/services/voice/eot-classifier-ggml.d.ts.map +1 -0
  445. package/src/services/voice/eot-classifier-ggml.ts +566 -0
  446. package/src/services/voice/eot-classifier.d.ts +214 -0
  447. package/src/services/voice/eot-classifier.d.ts.map +1 -0
  448. package/src/services/voice/eot-classifier.ts +533 -0
  449. package/src/services/voice/errors.d.ts +20 -0
  450. package/src/services/voice/errors.d.ts.map +1 -0
  451. package/src/services/voice/errors.ts +32 -0
  452. package/src/services/voice/expressive-tags.d.ts +158 -0
  453. package/src/services/voice/expressive-tags.d.ts.map +1 -0
  454. package/src/services/voice/expressive-tags.ts +405 -0
  455. package/src/services/voice/ffi-bindings.d.ts +636 -0
  456. package/src/services/voice/ffi-bindings.d.ts.map +1 -0
  457. package/src/services/voice/ffi-bindings.test.ts +671 -0
  458. package/src/services/voice/ffi-bindings.ts +3050 -0
  459. package/src/services/voice/first-line-cache.d.ts +181 -0
  460. package/src/services/voice/first-line-cache.d.ts.map +1 -0
  461. package/src/services/voice/first-line-cache.ts +725 -0
  462. package/src/services/voice/fused-eot-scorer.d.ts +51 -0
  463. package/src/services/voice/fused-eot-scorer.d.ts.map +1 -0
  464. package/src/services/voice/fused-eot-scorer.ts +135 -0
  465. package/src/services/voice/index.d.ts +91 -0
  466. package/src/services/voice/index.d.ts.map +1 -0
  467. package/src/services/voice/index.ts +481 -0
  468. package/src/services/voice/kokoro/__tests__/kokoro-backend.test.ts +151 -0
  469. package/src/services/voice/kokoro/__tests__/kokoro-engine-bridge.real.test.ts +151 -0
  470. package/src/services/voice/kokoro/__tests__/kokoro-engine-bridge.test.ts +60 -0
  471. package/src/services/voice/kokoro/__tests__/kokoro-engine-discovery.test.ts +277 -0
  472. package/src/services/voice/kokoro/__tests__/kokoro-ffi-runtime.test.ts +235 -0
  473. package/src/services/voice/kokoro/__tests__/kokoro-runtime.test.ts +95 -0
  474. package/src/services/voice/kokoro/__tests__/phonemizer.test.ts +53 -0
  475. package/src/services/voice/kokoro/__tests__/runtime-selection.test.ts +231 -0
  476. package/src/services/voice/kokoro/__tests__/voices.test.ts +57 -0
  477. package/src/services/voice/kokoro/index.ts +79 -0
  478. package/src/services/voice/kokoro/kokoro-backend.d.ts +72 -0
  479. package/src/services/voice/kokoro/kokoro-backend.d.ts.map +1 -0
  480. package/src/services/voice/kokoro/kokoro-backend.ts +207 -0
  481. package/src/services/voice/kokoro/kokoro-engine-discovery.d.ts +58 -0
  482. package/src/services/voice/kokoro/kokoro-engine-discovery.d.ts.map +1 -0
  483. package/src/services/voice/kokoro/kokoro-engine-discovery.ts +177 -0
  484. package/src/services/voice/kokoro/kokoro-ffi-runtime.d.ts +75 -0
  485. package/src/services/voice/kokoro/kokoro-ffi-runtime.d.ts.map +1 -0
  486. package/src/services/voice/kokoro/kokoro-ffi-runtime.ts +233 -0
  487. package/src/services/voice/kokoro/kokoro-runtime.d.ts +100 -0
  488. package/src/services/voice/kokoro/kokoro-runtime.d.ts.map +1 -0
  489. package/src/services/voice/kokoro/kokoro-runtime.ts +170 -0
  490. package/src/services/voice/kokoro/phoneme-stream.ts +123 -0
  491. package/src/services/voice/kokoro/phonemizer.d.ts +50 -0
  492. package/src/services/voice/kokoro/phonemizer.d.ts.map +1 -0
  493. package/src/services/voice/kokoro/phonemizer.ts +344 -0
  494. package/src/services/voice/kokoro/pick-runtime.d.ts +61 -0
  495. package/src/services/voice/kokoro/pick-runtime.d.ts.map +1 -0
  496. package/src/services/voice/kokoro/pick-runtime.test.ts +91 -0
  497. package/src/services/voice/kokoro/pick-runtime.ts +130 -0
  498. package/src/services/voice/kokoro/runtime-selection.d.ts +92 -0
  499. package/src/services/voice/kokoro/runtime-selection.d.ts.map +1 -0
  500. package/src/services/voice/kokoro/runtime-selection.ts +237 -0
  501. package/src/services/voice/kokoro/types.d.ts +82 -0
  502. package/src/services/voice/kokoro/types.d.ts.map +1 -0
  503. package/src/services/voice/kokoro/types.ts +95 -0
  504. package/src/services/voice/kokoro/voice-presets.d.ts +23 -0
  505. package/src/services/voice/kokoro/voice-presets.d.ts.map +1 -0
  506. package/src/services/voice/kokoro/voice-presets.ts +129 -0
  507. package/src/services/voice/kokoro/voices.d.ts +30 -0
  508. package/src/services/voice/kokoro/voices.d.ts.map +1 -0
  509. package/src/services/voice/kokoro/voices.ts +64 -0
  510. package/src/services/voice/lifecycle.d.ts +135 -0
  511. package/src/services/voice/lifecycle.d.ts.map +1 -0
  512. package/src/services/voice/lifecycle.test.ts +315 -0
  513. package/src/services/voice/lifecycle.ts +301 -0
  514. package/src/services/voice/live-diarization-session.d.ts +96 -0
  515. package/src/services/voice/live-diarization-session.d.ts.map +1 -0
  516. package/src/services/voice/live-diarization-session.ts +289 -0
  517. package/src/services/voice/mic-source.d.ts +136 -0
  518. package/src/services/voice/mic-source.d.ts.map +1 -0
  519. package/src/services/voice/mic-source.test.ts +210 -0
  520. package/src/services/voice/mic-source.ts +503 -0
  521. package/src/services/voice/optimistic-policy.d.ts +109 -0
  522. package/src/services/voice/optimistic-policy.d.ts.map +1 -0
  523. package/src/services/voice/optimistic-policy.test.ts +101 -0
  524. package/src/services/voice/optimistic-policy.ts +192 -0
  525. package/src/services/voice/optimistic-rollback.ts +343 -0
  526. package/src/services/voice/partial-stabilizer.d.ts +73 -0
  527. package/src/services/voice/partial-stabilizer.d.ts.map +1 -0
  528. package/src/services/voice/partial-stabilizer.test.ts +68 -0
  529. package/src/services/voice/partial-stabilizer.ts +140 -0
  530. package/src/services/voice/phoneme-tokenizer.d.ts +49 -0
  531. package/src/services/voice/phoneme-tokenizer.d.ts.map +1 -0
  532. package/src/services/voice/phoneme-tokenizer.ts +158 -0
  533. package/src/services/voice/phrase-cache.d.ts +76 -0
  534. package/src/services/voice/phrase-cache.d.ts.map +1 -0
  535. package/src/services/voice/phrase-cache.test.ts +242 -0
  536. package/src/services/voice/phrase-cache.ts +186 -0
  537. package/src/services/voice/phrase-chunker.d.ts +62 -0
  538. package/src/services/voice/phrase-chunker.d.ts.map +1 -0
  539. package/src/services/voice/phrase-chunker.test.ts +239 -0
  540. package/src/services/voice/phrase-chunker.ts +281 -0
  541. package/src/services/voice/pipeline-impls.d.ts +151 -0
  542. package/src/services/voice/pipeline-impls.d.ts.map +1 -0
  543. package/src/services/voice/pipeline-impls.l6.test.ts +110 -0
  544. package/src/services/voice/pipeline-impls.test.ts +292 -0
  545. package/src/services/voice/pipeline-impls.ts +315 -0
  546. package/src/services/voice/pipeline.d.ts +216 -0
  547. package/src/services/voice/pipeline.d.ts.map +1 -0
  548. package/src/services/voice/pipeline.ts +505 -0
  549. package/src/services/voice/prefill-client.d.ts +123 -0
  550. package/src/services/voice/prefill-client.d.ts.map +1 -0
  551. package/src/services/voice/prefill-client.ts +316 -0
  552. package/src/services/voice/prefix-preserving-queue.d.ts +113 -0
  553. package/src/services/voice/prefix-preserving-queue.d.ts.map +1 -0
  554. package/src/services/voice/prefix-preserving-queue.ts +162 -0
  555. package/src/services/voice/profile-store.d.ts +248 -0
  556. package/src/services/voice/profile-store.d.ts.map +1 -0
  557. package/src/services/voice/profile-store.ts +887 -0
  558. package/src/services/voice/ring-buffer.d.ts +40 -0
  559. package/src/services/voice/ring-buffer.d.ts.map +1 -0
  560. package/src/services/voice/ring-buffer.ts +105 -0
  561. package/src/services/voice/rollback-queue.d.ts +24 -0
  562. package/src/services/voice/rollback-queue.d.ts.map +1 -0
  563. package/src/services/voice/rollback-queue.ts +74 -0
  564. package/src/services/voice/samantha-preset-placeholder.d.ts +67 -0
  565. package/src/services/voice/samantha-preset-placeholder.d.ts.map +1 -0
  566. package/src/services/voice/samantha-preset-placeholder.test.ts +97 -0
  567. package/src/services/voice/samantha-preset-placeholder.ts +148 -0
  568. package/src/services/voice/samantha-preset-regenerator.d.ts +87 -0
  569. package/src/services/voice/samantha-preset-regenerator.d.ts.map +1 -0
  570. package/src/services/voice/samantha-preset-regenerator.ts +393 -0
  571. package/src/services/voice/scheduler.d.ts +146 -0
  572. package/src/services/voice/scheduler.d.ts.map +1 -0
  573. package/src/services/voice/scheduler.t2.test.ts +141 -0
  574. package/src/services/voice/scheduler.ts +927 -0
  575. package/src/services/voice/shared-resources.d.ts +190 -0
  576. package/src/services/voice/shared-resources.d.ts.map +1 -0
  577. package/src/services/voice/shared-resources.ts +320 -0
  578. package/src/services/voice/speaker/attribution-pipeline.d.ts +74 -0
  579. package/src/services/voice/speaker/attribution-pipeline.d.ts.map +1 -0
  580. package/src/services/voice/speaker/attribution-pipeline.ts +386 -0
  581. package/src/services/voice/speaker/diarizer-fused.d.ts +59 -0
  582. package/src/services/voice/speaker/diarizer-fused.d.ts.map +1 -0
  583. package/src/services/voice/speaker/diarizer-fused.real.test.ts +100 -0
  584. package/src/services/voice/speaker/diarizer-fused.ts +154 -0
  585. package/src/services/voice/speaker/diarizer.d.ts +75 -0
  586. package/src/services/voice/speaker/diarizer.d.ts.map +1 -0
  587. package/src/services/voice/speaker/diarizer.ts +218 -0
  588. package/src/services/voice/speaker/encoder-fused.d.ts +60 -0
  589. package/src/services/voice/speaker/encoder-fused.d.ts.map +1 -0
  590. package/src/services/voice/speaker/encoder-fused.real.test.ts +113 -0
  591. package/src/services/voice/speaker/encoder-fused.ts +138 -0
  592. package/src/services/voice/speaker/encoder-ggml.d.ts +33 -0
  593. package/src/services/voice/speaker/encoder-ggml.d.ts.map +1 -0
  594. package/src/services/voice/speaker/encoder-ggml.ts +79 -0
  595. package/src/services/voice/speaker/encoder.d.ts +37 -0
  596. package/src/services/voice/speaker/encoder.d.ts.map +1 -0
  597. package/src/services/voice/speaker/encoder.ts +105 -0
  598. package/src/services/voice/speaker-imprint.d.ts +83 -0
  599. package/src/services/voice/speaker-imprint.d.ts.map +1 -0
  600. package/src/services/voice/speaker-imprint.test.ts +185 -0
  601. package/src/services/voice/speaker-imprint.ts +312 -0
  602. package/src/services/voice/speaker-preset-cache.d.ts +77 -0
  603. package/src/services/voice/speaker-preset-cache.d.ts.map +1 -0
  604. package/src/services/voice/speaker-preset-cache.test.ts +154 -0
  605. package/src/services/voice/speaker-preset-cache.ts +195 -0
  606. package/src/services/voice/streaming-asr/streaming-pipeline-adapter.ts +292 -0
  607. package/src/services/voice/system-audio-sink.d.ts +73 -0
  608. package/src/services/voice/system-audio-sink.d.ts.map +1 -0
  609. package/src/services/voice/system-audio-sink.test.ts +29 -0
  610. package/src/services/voice/system-audio-sink.ts +366 -0
  611. package/src/services/voice/transcriber.d.ts +244 -0
  612. package/src/services/voice/transcriber.d.ts.map +1 -0
  613. package/src/services/voice/transcriber.test.ts +392 -0
  614. package/src/services/voice/transcriber.ts +704 -0
  615. package/src/services/voice/turn-controller.d.ts +183 -0
  616. package/src/services/voice/turn-controller.d.ts.map +1 -0
  617. package/src/services/voice/turn-controller.test.ts +575 -0
  618. package/src/services/voice/turn-controller.ts +596 -0
  619. package/src/services/voice/types.d.ts +643 -0
  620. package/src/services/voice/types.d.ts.map +1 -0
  621. package/src/services/voice/types.ts +699 -0
  622. package/src/services/voice/vad.d.ts +282 -0
  623. package/src/services/voice/vad.d.ts.map +1 -0
  624. package/src/services/voice/vad.test.ts +480 -0
  625. package/src/services/voice/vad.ts +827 -0
  626. package/src/services/voice/vad.v1-v4.test.ts +222 -0
  627. package/src/services/voice/voice-budget.d.ts +241 -0
  628. package/src/services/voice/voice-budget.d.ts.map +1 -0
  629. package/src/services/voice/voice-budget.test.ts +420 -0
  630. package/src/services/voice/voice-budget.ts +656 -0
  631. package/src/services/voice/voice-duet.test.ts +375 -0
  632. package/src/services/voice/voice-emotion-classifier.d.ts +95 -0
  633. package/src/services/voice/voice-emotion-classifier.d.ts.map +1 -0
  634. package/src/services/voice/voice-emotion-classifier.test.ts +210 -0
  635. package/src/services/voice/voice-emotion-classifier.ts +273 -0
  636. package/src/services/voice/voice-preset-format.d.ts +158 -0
  637. package/src/services/voice/voice-preset-format.d.ts.map +1 -0
  638. package/src/services/voice/voice-preset-format.ts +700 -0
  639. package/src/services/voice/voice-preset-generator.test.ts +89 -0
  640. package/src/services/voice/voice-profile-artifact.d.ts +116 -0
  641. package/src/services/voice/voice-profile-artifact.d.ts.map +1 -0
  642. package/src/services/voice/voice-profile-artifact.test.ts +138 -0
  643. package/src/services/voice/voice-profile-artifact.ts +518 -0
  644. package/src/services/voice/voice-profile-routes.d.ts +83 -0
  645. package/src/services/voice/voice-profile-routes.d.ts.map +1 -0
  646. package/src/services/voice/voice-profile-routes.test.ts +429 -0
  647. package/src/services/voice/voice-profile-routes.ts +425 -0
  648. package/src/services/voice/voice-scenario.ts +154 -0
  649. package/src/services/voice/voice-settings.d.ts +82 -0
  650. package/src/services/voice/voice-settings.d.ts.map +1 -0
  651. package/src/services/voice/voice-settings.ts +172 -0
  652. package/src/services/voice/voice-state-machine.d.ts +364 -0
  653. package/src/services/voice/voice-state-machine.d.ts.map +1 -0
  654. package/src/services/voice/voice-state-machine.ts +727 -0
  655. package/src/services/voice/voice-workbench-report.test.ts +168 -0
  656. package/src/services/voice/voice-workbench-report.ts +326 -0
  657. package/src/services/voice/voice-workbench.test.ts +158 -0
  658. package/src/services/voice/voice.test.ts +1070 -0
  659. package/src/services/voice/wake-word-ggml.d.ts +101 -0
  660. package/src/services/voice/wake-word-ggml.d.ts.map +1 -0
  661. package/src/services/voice/wake-word-ggml.ts +320 -0
  662. package/src/services/voice/wake-word.d.ts +255 -0
  663. package/src/services/voice/wake-word.d.ts.map +1 -0
  664. package/src/services/voice/wake-word.test.ts +298 -0
  665. package/src/services/voice/wake-word.ts +554 -0
  666. package/src/services/voice/wrap-with-first-line-cache.d.ts +70 -0
  667. package/src/services/voice/wrap-with-first-line-cache.d.ts.map +1 -0
  668. package/src/services/voice/wrap-with-first-line-cache.ts +267 -0
  669. package/src/services/voice-model-updater.d.ts +240 -0
  670. package/src/services/voice-model-updater.d.ts.map +1 -0
  671. package/src/services/voice-model-updater.ts +724 -0
  672. package/src/services/voice-prewarm.d.ts +3 -0
  673. package/src/services/voice-prewarm.d.ts.map +1 -0
  674. package/src/services/voice-prewarm.ts +51 -0
  675. package/dist/index.d.ts +0 -37
  676. package/dist/index.js +0 -1098
@@ -0,0 +1,899 @@
1
+ /**
2
+ * Resumable GGUF downloader.
3
+ *
4
+ * Streams directly from HuggingFace to a staging file under
5
+ * `$STATE_DIR/local-inference/downloads/<id>.part`, then atomically moves
6
+ * it into `models/<id>.gguf` on success. On restart the staging file is
7
+ * still there; `resumeIfPossible` sends a Range request starting at the
8
+ * current partial size.
9
+ *
10
+ * Concurrency model: at most one download per model id. Callers use
11
+ * `subscribe()` to receive progress events; the service facade wires that
12
+ * to SSE.
13
+ *
14
+ * The runtime `fetch` follows HuggingFace redirects and still gives us a body
15
+ * stream that can be piped into a Node WriteStream.
16
+ */
17
+
18
+ import { randomUUID } from "node:crypto";
19
+ import fs from "node:fs";
20
+ import fsp from "node:fs/promises";
21
+ import path from "node:path";
22
+ import { Readable, type Writable } from "node:stream";
23
+ import { pipeline } from "node:stream/promises";
24
+ import { ensureDefaultAssignment } from "./assignments";
25
+ import {
26
+ buildHuggingFaceResolveUrl,
27
+ buildHuggingFaceResolveUrlForPath,
28
+ findCatalogModel,
29
+ isDefaultEligibleId,
30
+ } from "./catalog";
31
+ import { deviceCapsFromProbe, probeHardware } from "./hardware";
32
+ import {
33
+ type Eliza1DeviceCaps,
34
+ type Eliza1FileEntry,
35
+ type Eliza1Files,
36
+ type Eliza1Manifest,
37
+ parseManifestOrThrow,
38
+ SUPPORTED_BACKENDS_BY_TIER,
39
+ } from "./manifest";
40
+ import {
41
+ downloadsStagingDir,
42
+ elizaModelsDir,
43
+ localInferenceRoot,
44
+ } from "./paths";
45
+ import { upsertElizaModel } from "./registry";
46
+ import type {
47
+ CatalogModel,
48
+ DownloadEvent,
49
+ DownloadJob,
50
+ DownloadState,
51
+ InstalledModel,
52
+ } from "./types";
53
+ import { hashFile } from "./verify";
54
+
55
+ interface ActiveJob {
56
+ job: DownloadJob;
57
+ abortController: AbortController;
58
+ stagingPath: string;
59
+ finalPath: string;
60
+ }
61
+
62
+ type DownloadListener = (event: DownloadEvent) => void;
63
+ type BundleFileKind = keyof Eliza1Files;
64
+
65
+ /**
66
+ * Thrown before any weight byte is fetched when an Eliza-1 bundle's manifest
67
+ * is incompatible with this device — wrong schema version, no overlapping
68
+ * verified backend, or a RAM budget that exceeds the device's memory. Per
69
+ * `packages/inference/AGENTS.md` §7 there is no "download anyway" path.
70
+ */
71
+ export class BundleIncompatibleError extends Error {
72
+ readonly code = "ELIZA1_BUNDLE_INCOMPATIBLE" as const;
73
+ constructor(message: string) {
74
+ super(message);
75
+ this.name = "BundleIncompatibleError";
76
+ }
77
+ }
78
+
79
+ /**
80
+ * One-time verify-on-device pass per `packages/inference/AGENTS.md` §7:
81
+ * load → 1-token text generation → 1-phrase voice generation → barge-in
82
+ * cancel. The downloader stays decoupled from the engine — the service
83
+ * layer injects this; when absent the bundle is materialized and registered
84
+ * but its `bundleVerifiedAt` stays unset and it does NOT auto-fill an empty
85
+ * default slot (an unverified bundle must not become the recommended
86
+ * default).
87
+ */
88
+ export type VerifyBundleOnDevice = (args: {
89
+ modelId: string;
90
+ bundleRoot: string;
91
+ manifestPath: string;
92
+ textGgufPath: string;
93
+ }) => Promise<void>;
94
+
95
+ export interface DownloaderOptions {
96
+ /** Override the device-capability probe (tests / headless environments). */
97
+ probeDeviceCaps?: () => Promise<Eliza1DeviceCaps>;
98
+ /** Verify-on-device smoke run; see {@link VerifyBundleOnDevice}. */
99
+ verifyOnDevice?: VerifyBundleOnDevice;
100
+ }
101
+
102
+ async function defaultProbeDeviceCaps(): Promise<Eliza1DeviceCaps> {
103
+ return deviceCapsFromProbe(await probeHardware());
104
+ }
105
+
106
+ /**
107
+ * Reject bundles this device cannot run — runs against the manifest before
108
+ * any weight byte is fetched. Mirrors the publish-side `canSetAsDefault`
109
+ * device check, minus the `defaultEligible` flag (a user may explicitly
110
+ * install a non-default bundle, but only one the device can actually load).
111
+ */
112
+ function assertBundleInstallable(
113
+ manifest: Eliza1Manifest,
114
+ device: Eliza1DeviceCaps,
115
+ ): void {
116
+ // Schema version is enforced upstream by `parseManifestOrThrow` — the Zod
117
+ // schema only accepts the current `$schema` URL, so a manifest with a
118
+ // non-current schema version is rejected before we get here.
119
+ if (manifest.ramBudgetMb.min > device.ramMb) {
120
+ throw new BundleIncompatibleError(
121
+ `Eliza-1 bundle ${manifest.id} needs at least ${manifest.ramBudgetMb.min} MB RAM; this device has ${device.ramMb} MB`,
122
+ );
123
+ }
124
+ const tierBackends = new Set(SUPPORTED_BACKENDS_BY_TIER[manifest.tier]);
125
+ const usable = device.availableBackends.filter(
126
+ (b) =>
127
+ tierBackends.has(b) &&
128
+ manifest.kernels.verifiedBackends[b].status === "pass",
129
+ );
130
+ if (usable.length === 0) {
131
+ const verified = Object.entries(manifest.kernels.verifiedBackends)
132
+ .filter(([, v]) => v.status === "pass")
133
+ .map(([b]) => b);
134
+ throw new BundleIncompatibleError(
135
+ `Eliza-1 bundle ${manifest.id}: no required-kernel backend is available on this device. ` +
136
+ `bundle verified [${verified.join(", ") || "none"}], device has [${device.availableBackends.join(", ")}], tier ${manifest.tier} supports [${[...tierBackends].join(", ")}]`,
137
+ );
138
+ }
139
+ }
140
+
141
+ interface DownloadedFile {
142
+ path: string;
143
+ sizeBytes: number;
144
+ sha256: string;
145
+ }
146
+
147
+ const PROGRESS_THROTTLE_MS = 250;
148
+ const TERMINAL_DOWNLOADS_FILENAME = "download-status.json";
149
+ const TERMINAL_DOWNLOAD_LIMIT = 32;
150
+
151
+ interface TerminalDownloadsFile {
152
+ version: 1;
153
+ jobs: DownloadJob[];
154
+ }
155
+
156
+ async function* readFetchBody(
157
+ body: ReadableStream<Uint8Array>,
158
+ ): AsyncIterable<Buffer> {
159
+ const reader = body.getReader();
160
+ try {
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) return;
164
+ if (value) yield Buffer.from(value);
165
+ }
166
+ } finally {
167
+ reader.releaseLock();
168
+ }
169
+ }
170
+
171
+ function stagingFilename(modelId: string): string {
172
+ // Filename is derived deterministically so repeated download attempts
173
+ // reuse the same partial file and actually resume.
174
+ const safe = modelId.replace(/[^a-zA-Z0-9._-]/g, "_");
175
+ return `${safe}.part`;
176
+ }
177
+
178
+ function finalFilename(model: CatalogModel): string {
179
+ const safe = model.id.replace(/[^a-zA-Z0-9._-]/g, "_");
180
+ return `${safe}.gguf`;
181
+ }
182
+
183
+ function bundleDirname(modelId: string): string {
184
+ const safe = modelId.replace(/[^a-zA-Z0-9._-]/g, "_");
185
+ return `${safe}.bundle`;
186
+ }
187
+
188
+ function bundleStagingFilename(modelId: string, filePath: string): string {
189
+ const safePath = filePath.replace(/[^a-zA-Z0-9._-]/g, "_");
190
+ return stagingFilename(`${modelId}__${safePath}`);
191
+ }
192
+
193
+ function bundleTargetPath(root: string, filePath: string): string {
194
+ if (
195
+ !filePath ||
196
+ path.isAbsolute(filePath) ||
197
+ /^[a-zA-Z]:[\\/]/.test(filePath)
198
+ ) {
199
+ throw new Error(`Invalid bundle file path: ${filePath}`);
200
+ }
201
+ const resolvedRoot = path.resolve(root);
202
+ const target = path.resolve(resolvedRoot, filePath);
203
+ if (
204
+ target !== resolvedRoot &&
205
+ !target.startsWith(`${resolvedRoot}${path.sep}`)
206
+ ) {
207
+ throw new Error(`Bundle file escapes install root: ${filePath}`);
208
+ }
209
+ return target;
210
+ }
211
+
212
+ function parseBundleManifestOrThrow(
213
+ input: unknown,
214
+ catalogEntry: CatalogModel,
215
+ ): Eliza1Manifest {
216
+ const manifest = parseManifestOrThrow(input);
217
+ if (manifest.id !== catalogEntry.id) {
218
+ throw new Error(
219
+ `Invalid Eliza-1 manifest: id ${manifest.id} does not match ${catalogEntry.id}`,
220
+ );
221
+ }
222
+ if (
223
+ !manifest.files.text.some((entry) => entry.path === catalogEntry.ggufFile)
224
+ ) {
225
+ throw new Error(
226
+ `Invalid Eliza-1 manifest: primary text file ${catalogEntry.ggufFile} is missing`,
227
+ );
228
+ }
229
+
230
+ return manifest;
231
+ }
232
+
233
+ function collectBundleFiles(
234
+ manifest: Eliza1Manifest,
235
+ ): Array<{ kind: BundleFileKind; entry: Eliza1FileEntry }> {
236
+ const seen = new Map<
237
+ string,
238
+ { kind: BundleFileKind; entry: Eliza1FileEntry }
239
+ >();
240
+ for (const kind of [
241
+ "text",
242
+ "voice",
243
+ "asr",
244
+ "vision",
245
+ "mtp",
246
+ "cache",
247
+ "embedding",
248
+ "vad",
249
+ "wakeword",
250
+ ] as const) {
251
+ for (const entry of manifest.files[kind] ?? []) {
252
+ const current = seen.get(entry.path);
253
+ if (current && current.entry.sha256 !== entry.sha256) {
254
+ throw new Error(
255
+ `Conflicting sha256 entries for bundle file ${entry.path}`,
256
+ );
257
+ }
258
+ seen.set(entry.path, { kind, entry });
259
+ }
260
+ }
261
+ return [...seen.values()];
262
+ }
263
+
264
+ async function ensureDirs(): Promise<void> {
265
+ await fsp.mkdir(downloadsStagingDir(), { recursive: true });
266
+ await fsp.mkdir(elizaModelsDir(), { recursive: true });
267
+ }
268
+
269
+ function terminalDownloadsPath(): string {
270
+ return path.join(localInferenceRoot(), TERMINAL_DOWNLOADS_FILENAME);
271
+ }
272
+
273
+ async function partialSize(stagingPath: string): Promise<number> {
274
+ try {
275
+ const stat = await fsp.stat(stagingPath);
276
+ return stat.isFile() ? stat.size : 0;
277
+ } catch {
278
+ return 0;
279
+ }
280
+ }
281
+
282
+ export class Downloader {
283
+ private readonly active = new Map<string, ActiveJob>();
284
+ private readonly terminal = new Map<string, DownloadJob>();
285
+ private readonly listeners = new Set<DownloadListener>();
286
+ private readonly lastEmit = new Map<string, number>();
287
+ private readonly probeDeviceCaps: () => Promise<Eliza1DeviceCaps>;
288
+ private readonly verifyOnDevice?: VerifyBundleOnDevice;
289
+
290
+ constructor(options: DownloaderOptions = {}) {
291
+ this.probeDeviceCaps = options.probeDeviceCaps ?? defaultProbeDeviceCaps;
292
+ this.verifyOnDevice = options.verifyOnDevice;
293
+ this.loadTerminalDownloads();
294
+ }
295
+
296
+ subscribe(listener: DownloadListener): () => void {
297
+ this.listeners.add(listener);
298
+ return () => {
299
+ this.listeners.delete(listener);
300
+ };
301
+ }
302
+
303
+ snapshot(): DownloadJob[] {
304
+ const active = [...this.active.values()].map((a) => ({ ...a.job }));
305
+ const activeIds = new Set(active.map((job) => job.modelId));
306
+ const terminal = [...this.terminal.values()]
307
+ .filter((job) => !activeIds.has(job.modelId))
308
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
309
+ .map((job) => ({ ...job }));
310
+ return [...active, ...terminal];
311
+ }
312
+
313
+ isActive(modelId: string): boolean {
314
+ const current = this.active.get(modelId);
315
+ return (
316
+ !!current &&
317
+ (current.job.state === "queued" || current.job.state === "downloading")
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Start a download for a model. Accepts either a curated catalog id, or
323
+ * a full `CatalogModel` spec for ad-hoc HF-search results. Idempotent —
324
+ * returns the existing job if one is already running for the same id.
325
+ */
326
+ async start(modelIdOrSpec: string | CatalogModel): Promise<DownloadJob> {
327
+ const catalogEntry =
328
+ typeof modelIdOrSpec === "string"
329
+ ? findCatalogModel(modelIdOrSpec)
330
+ : modelIdOrSpec;
331
+ if (!catalogEntry) {
332
+ throw new Error(
333
+ `Unknown model id: ${typeof modelIdOrSpec === "string" ? modelIdOrSpec : "(no id)"}`,
334
+ );
335
+ }
336
+ const modelId = catalogEntry.id;
337
+ this.clearTerminalDownload(modelId);
338
+
339
+ const existing = this.active.get(modelId);
340
+ if (
341
+ existing &&
342
+ (existing.job.state === "queued" || existing.job.state === "downloading")
343
+ ) {
344
+ return { ...existing.job };
345
+ }
346
+
347
+ await ensureDirs();
348
+ const stagingPath = path.join(
349
+ downloadsStagingDir(),
350
+ stagingFilename(modelId),
351
+ );
352
+ const finalPath = path.join(elizaModelsDir(), finalFilename(catalogEntry));
353
+
354
+ const job: DownloadJob = {
355
+ jobId: randomUUID(),
356
+ modelId,
357
+ state: "queued",
358
+ received: await partialSize(stagingPath),
359
+ total: Math.round(catalogEntry.sizeGb * 1024 ** 3),
360
+ bytesPerSec: 0,
361
+ etaMs: null,
362
+ startedAt: new Date().toISOString(),
363
+ updatedAt: new Date().toISOString(),
364
+ };
365
+
366
+ const abortController = new AbortController();
367
+ const record: ActiveJob = {
368
+ job,
369
+ abortController,
370
+ stagingPath,
371
+ finalPath,
372
+ };
373
+ this.active.set(modelId, record);
374
+
375
+ // Fire-and-forget; errors are captured and emitted as a "failed" event.
376
+ void this.runJob(catalogEntry, record).catch(() => {
377
+ // `runJob` handles its own failure telemetry; we only need to swallow
378
+ // the unhandled-rejection here.
379
+ });
380
+
381
+ this.emit({ type: "progress", job: { ...job } });
382
+ return { ...job };
383
+ }
384
+
385
+ cancel(modelId: string): boolean {
386
+ const record = this.active.get(modelId);
387
+ if (!record) return false;
388
+ if (record.job.state !== "downloading" && record.job.state !== "queued") {
389
+ return false;
390
+ }
391
+ record.abortController.abort();
392
+ this.updateState(record, "cancelled");
393
+ this.rememberTerminalDownload(record.job);
394
+ this.emit({ type: "cancelled", job: { ...record.job } });
395
+ this.active.delete(modelId);
396
+ return true;
397
+ }
398
+
399
+ private emit(event: DownloadEvent): void {
400
+ for (const listener of this.listeners) {
401
+ try {
402
+ listener(event);
403
+ } catch {
404
+ // A bad listener must not kill the downloader; drop it silently.
405
+ this.listeners.delete(listener);
406
+ }
407
+ }
408
+ }
409
+
410
+ private updateState(record: ActiveJob, state: DownloadState): void {
411
+ record.job.state = state;
412
+ record.job.updatedAt = new Date().toISOString();
413
+ }
414
+
415
+ private loadTerminalDownloads(): void {
416
+ try {
417
+ const raw = fs.readFileSync(terminalDownloadsPath(), "utf8");
418
+ const parsed = JSON.parse(raw) as TerminalDownloadsFile;
419
+ if (parsed?.version !== 1 || !Array.isArray(parsed.jobs)) {
420
+ return;
421
+ }
422
+ for (const job of parsed.jobs) {
423
+ if (
424
+ job &&
425
+ typeof job.modelId === "string" &&
426
+ (job.state === "completed" ||
427
+ job.state === "failed" ||
428
+ job.state === "cancelled")
429
+ ) {
430
+ this.terminal.set(job.modelId, { ...job });
431
+ }
432
+ }
433
+ } catch {
434
+ // Missing or malformed terminal-download state should not block
435
+ // local inference. New terminal states will rewrite the file.
436
+ }
437
+ }
438
+
439
+ private persistTerminalDownloads(): void {
440
+ try {
441
+ fs.mkdirSync(localInferenceRoot(), { recursive: true });
442
+ const jobs = [...this.terminal.values()]
443
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
444
+ .slice(0, TERMINAL_DOWNLOAD_LIMIT);
445
+ const payload: TerminalDownloadsFile = { version: 1, jobs };
446
+ fs.writeFileSync(
447
+ terminalDownloadsPath(),
448
+ JSON.stringify(payload, null, 2),
449
+ "utf8",
450
+ );
451
+ } catch {
452
+ // Terminal status is useful for chat/UI telemetry but is not allowed to
453
+ // fail the download path.
454
+ }
455
+ }
456
+
457
+ private rememberTerminalDownload(job: DownloadJob): void {
458
+ this.terminal.set(job.modelId, { ...job });
459
+ const ordered = [...this.terminal.values()].sort((left, right) =>
460
+ right.updatedAt.localeCompare(left.updatedAt),
461
+ );
462
+ this.terminal.clear();
463
+ for (const terminalJob of ordered.slice(0, TERMINAL_DOWNLOAD_LIMIT)) {
464
+ this.terminal.set(terminalJob.modelId, terminalJob);
465
+ }
466
+ this.persistTerminalDownloads();
467
+ }
468
+
469
+ private clearTerminalDownload(modelId: string): void {
470
+ if (!this.terminal.delete(modelId)) return;
471
+ this.persistTerminalDownloads();
472
+ }
473
+
474
+ private throttleEmit(record: ActiveJob): void {
475
+ const now = Date.now();
476
+ const last = this.lastEmit.get(record.job.modelId) ?? 0;
477
+ if (now - last < PROGRESS_THROTTLE_MS) return;
478
+ this.lastEmit.set(record.job.modelId, now);
479
+ this.emit({ type: "progress", job: { ...record.job } });
480
+ }
481
+
482
+ private async runJob(
483
+ catalogEntry: CatalogModel,
484
+ record: ActiveJob,
485
+ ): Promise<void> {
486
+ try {
487
+ this.updateState(record, "downloading");
488
+ if (catalogEntry.bundleManifestFile) {
489
+ await this.runBundleJob(catalogEntry, record);
490
+ return;
491
+ }
492
+
493
+ const url = buildHuggingFaceResolveUrl(catalogEntry);
494
+
495
+ const httpClient = await this.loadHttpClient();
496
+ const startByte = record.job.received;
497
+
498
+ const headers: Record<string, string> = {
499
+ "user-agent": "Eliza-LocalInference/1.0",
500
+ };
501
+ if (startByte > 0) {
502
+ headers.range = `bytes=${startByte}-`;
503
+ }
504
+
505
+ const response = await httpClient.request(url, {
506
+ method: "GET",
507
+ headers,
508
+ signal: record.abortController.signal,
509
+ });
510
+
511
+ if (response.statusCode >= 400) {
512
+ throw new Error(
513
+ `HTTP ${response.statusCode} from model hub for ${catalogEntry.hfRepo}`,
514
+ );
515
+ }
516
+ let effectiveStartByte = startByte;
517
+ if (effectiveStartByte > 0 && response.statusCode !== 206) {
518
+ effectiveStartByte = 0;
519
+ record.job.received = 0;
520
+ }
521
+
522
+ const contentLengthHeader = response.headers["content-length"];
523
+ const contentLength = Array.isArray(contentLengthHeader)
524
+ ? Number.parseInt(contentLengthHeader[0] ?? "0", 10)
525
+ : Number.parseInt(contentLengthHeader ?? "0", 10);
526
+ if (Number.isFinite(contentLength) && contentLength > 0) {
527
+ record.job.total = effectiveStartByte + contentLength;
528
+ }
529
+
530
+ const writeStream: Writable = fs.createWriteStream(record.stagingPath, {
531
+ flags: effectiveStartByte > 0 ? "a" : "w",
532
+ });
533
+
534
+ let lastSampleBytes = record.job.received;
535
+ let lastSampleAt = Date.now();
536
+
537
+ const bodyStream = Readable.from(response.body);
538
+ bodyStream.on("data", (chunk: Buffer) => {
539
+ record.job.received += chunk.length;
540
+
541
+ const now = Date.now();
542
+ const elapsed = now - lastSampleAt;
543
+ if (elapsed >= 1000) {
544
+ record.job.bytesPerSec =
545
+ ((record.job.received - lastSampleBytes) * 1000) / elapsed;
546
+ record.job.etaMs =
547
+ record.job.bytesPerSec > 0
548
+ ? ((record.job.total - record.job.received) * 1000) /
549
+ record.job.bytesPerSec
550
+ : null;
551
+ lastSampleAt = now;
552
+ lastSampleBytes = record.job.received;
553
+ }
554
+
555
+ this.throttleEmit(record);
556
+ });
557
+
558
+ await pipeline(bodyStream, writeStream);
559
+
560
+ await fsp.rename(record.stagingPath, record.finalPath);
561
+
562
+ const finalStat = await fsp.stat(record.finalPath);
563
+ // Compute SHA256 on commit so we have an integrity baseline. The
564
+ // chunk hasher we maintain during streaming gives the same result
565
+ // but would also have to handle resume-from-partial correctly; for
566
+ // a ~1-20 GB file a second disk pass at the end is simpler and
567
+ // robust. Measured at ~400 MB/s on an NVMe so even the 20 GB
568
+ // catalog entries finish in well under a minute.
569
+ const sha256 = await hashFile(record.finalPath);
570
+
571
+ const installed: InstalledModel = {
572
+ id: catalogEntry.id,
573
+ displayName: catalogEntry.displayName,
574
+ path: record.finalPath,
575
+ sizeBytes: finalStat.size,
576
+ hfRepo: catalogEntry.hfRepo,
577
+ installedAt: new Date().toISOString(),
578
+ lastUsedAt: null,
579
+ source: "eliza-download",
580
+ sha256,
581
+ lastVerifiedAt: new Date().toISOString(),
582
+ ...(catalogEntry.runtimeRole
583
+ ? { runtimeRole: catalogEntry.runtimeRole }
584
+ : {}),
585
+ };
586
+ await upsertElizaModel(installed);
587
+
588
+ // First-light convenience: only default-eligible Eliza-1 downloads
589
+ // can fill empty slots. Ad-hoc Hugging Face downloads remain
590
+ // explicit opt-in even though Eliza owns the downloaded file.
591
+ if (isDefaultEligibleId(installed.id)) {
592
+ await ensureDefaultAssignment(installed.id);
593
+ }
594
+
595
+ this.updateState(record, "completed");
596
+ record.job.received = finalStat.size;
597
+ record.job.total = finalStat.size;
598
+ this.rememberTerminalDownload(record.job);
599
+ this.emit({ type: "completed", job: { ...record.job } });
600
+ } catch (err) {
601
+ if (record.abortController.signal.aborted) {
602
+ this.updateState(record, "cancelled");
603
+ this.rememberTerminalDownload(record.job);
604
+ this.emit({ type: "cancelled", job: { ...record.job } });
605
+ } else {
606
+ this.updateState(record, "failed");
607
+ record.job.error = err instanceof Error ? err.message : String(err);
608
+ this.rememberTerminalDownload(record.job);
609
+ this.emit({ type: "failed", job: { ...record.job } });
610
+ }
611
+ } finally {
612
+ this.active.delete(record.job.modelId);
613
+ }
614
+ }
615
+
616
+ private async runBundleJob(
617
+ catalogEntry: CatalogModel,
618
+ record: ActiveJob,
619
+ ): Promise<void> {
620
+ if (!catalogEntry.bundleManifestFile) {
621
+ throw new Error(
622
+ `[local-inference] ${catalogEntry.id} has no bundle manifest`,
623
+ );
624
+ }
625
+
626
+ const bundleRoot = path.join(
627
+ elizaModelsDir(),
628
+ bundleDirname(catalogEntry.id),
629
+ );
630
+ await fsp.mkdir(bundleRoot, { recursive: true });
631
+
632
+ const manifestPath = bundleTargetPath(
633
+ bundleRoot,
634
+ catalogEntry.bundleManifestFile,
635
+ );
636
+ const manifestDownloaded = await this.downloadRemotePath(
637
+ catalogEntry,
638
+ catalogEntry.bundleManifestFile,
639
+ path.join(
640
+ downloadsStagingDir(),
641
+ bundleStagingFilename(catalogEntry.id, catalogEntry.bundleManifestFile),
642
+ ),
643
+ manifestPath,
644
+ record,
645
+ 0,
646
+ catalogEntry.bundleManifestSha256,
647
+ );
648
+
649
+ const manifest = parseBundleManifestOrThrow(
650
+ JSON.parse(await fsp.readFile(manifestPath, "utf8")),
651
+ catalogEntry,
652
+ );
653
+
654
+ // §7: schema version, RAM budget, and kernel-backend availability are
655
+ // checked against this device BEFORE any weight byte is fetched. An
656
+ // incompatible bundle aborts here — there is no "download anyway" path.
657
+ assertBundleInstallable(manifest, await this.probeDeviceCaps());
658
+
659
+ let completedBytes = manifestDownloaded.sizeBytes;
660
+ const downloaded = new Map<string, DownloadedFile>();
661
+ for (const { entry } of collectBundleFiles(manifest)) {
662
+ const finalPath = bundleTargetPath(bundleRoot, entry.path);
663
+ const result = await this.downloadRemotePath(
664
+ catalogEntry,
665
+ entry.path,
666
+ path.join(
667
+ downloadsStagingDir(),
668
+ bundleStagingFilename(catalogEntry.id, entry.path),
669
+ ),
670
+ finalPath,
671
+ record,
672
+ completedBytes,
673
+ entry.sha256,
674
+ );
675
+ downloaded.set(entry.path, result);
676
+ completedBytes += result.sizeBytes;
677
+ record.job.received = completedBytes;
678
+ record.job.total = Math.max(record.job.total, completedBytes);
679
+ this.throttleEmit(record);
680
+ }
681
+
682
+ const textEntry = manifest.files.text.find(
683
+ (entry) => entry.path === catalogEntry.ggufFile,
684
+ );
685
+ if (!textEntry) {
686
+ throw new Error(
687
+ `[local-inference] Bundle missing primary text file ${catalogEntry.ggufFile}`,
688
+ );
689
+ }
690
+ const textFile = downloaded.get(textEntry.path);
691
+ if (!textFile) {
692
+ throw new Error(
693
+ `[local-inference] Bundle did not install text file ${textEntry.path}`,
694
+ );
695
+ }
696
+
697
+ // §7: materialize the bundle, then run the one-time verify-on-device
698
+ // pass before the bundle is treated as ready. The hook is injected by
699
+ // the service layer so the downloader stays decoupled from the engine.
700
+ // When no hook is wired, `bundleVerifiedAt` stays unset and the bundle
701
+ // is registered but does NOT auto-fill an empty default slot.
702
+ let bundleVerifiedAt: string | undefined;
703
+ if (this.verifyOnDevice) {
704
+ await this.verifyOnDevice({
705
+ modelId: catalogEntry.id,
706
+ bundleRoot,
707
+ manifestPath,
708
+ textGgufPath: textFile.path,
709
+ });
710
+ bundleVerifiedAt = new Date().toISOString();
711
+ }
712
+
713
+ const now = new Date().toISOString();
714
+ const bundleMeta = {
715
+ bundleRoot,
716
+ manifestPath,
717
+ manifestSha256: manifestDownloaded.sha256,
718
+ bundleVersion: manifest.version,
719
+ bundleSizeBytes: completedBytes,
720
+ ...(bundleVerifiedAt ? { bundleVerifiedAt } : {}),
721
+ };
722
+
723
+ const installed: InstalledModel = {
724
+ id: catalogEntry.id,
725
+ displayName: catalogEntry.displayName,
726
+ path: textFile.path,
727
+ sizeBytes: textFile.sizeBytes,
728
+ hfRepo: catalogEntry.hfRepo,
729
+ installedAt: now,
730
+ lastUsedAt: null,
731
+ source: "eliza-download",
732
+ sha256: textFile.sha256,
733
+ lastVerifiedAt: now,
734
+ ...bundleMeta,
735
+ };
736
+ await upsertElizaModel(installed);
737
+
738
+ // An empty default slot is filled only after the on-device verify pass
739
+ // succeeds. Without a verify hook the bundle is installed and visible,
740
+ // but it is not allowed to auto-fill defaults.
741
+ if (isDefaultEligibleId(installed.id) && bundleVerifiedAt !== undefined) {
742
+ await ensureDefaultAssignment(installed.id);
743
+ }
744
+
745
+ this.updateState(record, "completed");
746
+ record.job.received = completedBytes;
747
+ record.job.total = completedBytes;
748
+ this.rememberTerminalDownload(record.job);
749
+ this.emit({ type: "completed", job: { ...record.job } });
750
+ }
751
+
752
+ private async downloadRemotePath(
753
+ catalogEntry: CatalogModel,
754
+ remotePath: string,
755
+ stagingPath: string,
756
+ finalPath: string,
757
+ record: ActiveJob,
758
+ baseBytes: number,
759
+ expectedSha256?: string,
760
+ ): Promise<DownloadedFile> {
761
+ if (expectedSha256) {
762
+ try {
763
+ const stat = await fsp.stat(finalPath);
764
+ if (stat.isFile()) {
765
+ const currentSha256 = await hashFile(finalPath);
766
+ if (currentSha256 === expectedSha256) {
767
+ record.job.received = baseBytes + stat.size;
768
+ return {
769
+ path: finalPath,
770
+ sizeBytes: stat.size,
771
+ sha256: currentSha256,
772
+ };
773
+ }
774
+ await fsp.rm(finalPath, { force: true });
775
+ }
776
+ } catch {
777
+ // Missing files are downloaded below; unreadable stale files are
778
+ // treated as invalid and replaced by the fresh bundle artifact.
779
+ }
780
+ } else {
781
+ await fsp.rm(stagingPath, { force: true }).catch(() => undefined);
782
+ }
783
+
784
+ await fsp.mkdir(path.dirname(finalPath), { recursive: true });
785
+ await fsp.mkdir(path.dirname(stagingPath), { recursive: true });
786
+
787
+ let startByte = expectedSha256 ? await partialSize(stagingPath) : 0;
788
+ record.job.received = baseBytes + startByte;
789
+
790
+ const headers: Record<string, string> = {
791
+ "user-agent": "Eliza-LocalInference/1.0",
792
+ };
793
+ if (startByte > 0) {
794
+ headers.range = `bytes=${startByte}-`;
795
+ }
796
+
797
+ const httpClient = await this.loadHttpClient();
798
+ const url = buildHuggingFaceResolveUrlForPath(catalogEntry, remotePath);
799
+ const response = await httpClient.request(url, {
800
+ method: "GET",
801
+ headers,
802
+ signal: record.abortController.signal,
803
+ });
804
+
805
+ if (response.statusCode >= 400) {
806
+ throw new Error(
807
+ `HTTP ${response.statusCode} from model hub for ${catalogEntry.hfRepo}/${remotePath}`,
808
+ );
809
+ }
810
+ if (startByte > 0 && response.statusCode !== 206) {
811
+ startByte = 0;
812
+ record.job.received = baseBytes;
813
+ }
814
+
815
+ const contentLengthHeader = response.headers["content-length"];
816
+ const contentLength = Array.isArray(contentLengthHeader)
817
+ ? Number.parseInt(contentLengthHeader[0] ?? "0", 10)
818
+ : Number.parseInt(contentLengthHeader ?? "0", 10);
819
+ if (Number.isFinite(contentLength) && contentLength > 0) {
820
+ record.job.total = Math.max(
821
+ record.job.total,
822
+ baseBytes + startByte + contentLength,
823
+ );
824
+ }
825
+
826
+ const writeStream: Writable = fs.createWriteStream(stagingPath, {
827
+ flags: startByte > 0 ? "a" : "w",
828
+ });
829
+
830
+ let lastSampleBytes = record.job.received;
831
+ let lastSampleAt = Date.now();
832
+ const bodyStream = Readable.from(response.body);
833
+ bodyStream.on("data", (chunk: Buffer) => {
834
+ record.job.received += chunk.length;
835
+
836
+ const now = Date.now();
837
+ const elapsed = now - lastSampleAt;
838
+ if (elapsed >= 1000) {
839
+ record.job.bytesPerSec =
840
+ ((record.job.received - lastSampleBytes) * 1000) / elapsed;
841
+ record.job.etaMs =
842
+ record.job.bytesPerSec > 0
843
+ ? ((record.job.total - record.job.received) * 1000) /
844
+ record.job.bytesPerSec
845
+ : null;
846
+ lastSampleAt = now;
847
+ lastSampleBytes = record.job.received;
848
+ }
849
+
850
+ this.throttleEmit(record);
851
+ });
852
+
853
+ await pipeline(bodyStream, writeStream);
854
+ await fsp.rename(stagingPath, finalPath);
855
+
856
+ const stat = await fsp.stat(finalPath);
857
+ const sha256 = await hashFile(finalPath);
858
+ if (expectedSha256 && sha256 !== expectedSha256) {
859
+ await fsp.rm(finalPath, { force: true });
860
+ throw new Error(`SHA256 mismatch for bundle file ${remotePath}`);
861
+ }
862
+ return { path: finalPath, sizeBytes: stat.size, sha256 };
863
+ }
864
+
865
+ private async loadHttpClient(): Promise<{
866
+ request: (
867
+ url: string,
868
+ options: {
869
+ method: string;
870
+ headers: Record<string, string>;
871
+ signal: AbortSignal;
872
+ },
873
+ ) => Promise<{
874
+ statusCode: number;
875
+ headers: Record<string, string | string[] | undefined>;
876
+ body: AsyncIterable<Buffer>;
877
+ }>;
878
+ }> {
879
+ const fetchImpl = globalThis.fetch;
880
+ return {
881
+ request: async (url, options) => {
882
+ const response = await fetchImpl(url, {
883
+ method: options.method,
884
+ headers: options.headers,
885
+ signal: options.signal,
886
+ redirect: "follow",
887
+ });
888
+ if (!response.body) {
889
+ throw new Error(`Empty response body from ${url}`);
890
+ }
891
+ return {
892
+ statusCode: response.status,
893
+ headers: Object.fromEntries(response.headers.entries()),
894
+ body: readFetchBody(response.body),
895
+ };
896
+ },
897
+ };
898
+ }
899
+ }