@qvac/sdk 0.13.5 → 0.14.1

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 (226) hide show
  1. package/NOTICE +95 -1151
  2. package/README.md +1 -2
  3. package/bare-imports.json +0 -5
  4. package/dist/client/api/bci-transcribe.js +2 -1
  5. package/dist/client/api/download-asset.js +2 -1
  6. package/dist/client/api/finetune.js +6 -5
  7. package/dist/client/api/index.d.ts +1 -0
  8. package/dist/client/api/index.js +1 -0
  9. package/dist/client/api/load-model.d.ts +11 -8
  10. package/dist/client/api/load-model.js +6 -6
  11. package/dist/client/api/subscribe-logs.d.ts +28 -0
  12. package/dist/client/api/subscribe-logs.js +43 -0
  13. package/dist/client/api/text-to-speech.js +2 -1
  14. package/dist/client/config-loader/config-utils.js +2 -4
  15. package/dist/client/config-loader/resolve-config.bare.js +4 -3
  16. package/dist/client/parse-input.d.ts +3 -0
  17. package/dist/client/parse-input.js +14 -0
  18. package/dist/client/rpc/expo-rpc-client.js +2 -11
  19. package/dist/client/rpc/node-rpc-client.js +8 -2
  20. package/dist/client/rpc/rpc-client.js +39 -7
  21. package/dist/commands/bundle/constants.js +2 -1
  22. package/dist/commands/bundle/entry-gen.d.ts +1 -1
  23. package/dist/commands/bundle/entry-gen.js +6 -6
  24. package/dist/commands/bundle/index.js +3 -1
  25. package/dist/commands/bundle/resolve-sdk-import.d.ts +6 -0
  26. package/dist/commands/bundle/resolve-sdk-import.js +60 -0
  27. package/dist/examples/bci/bci-filesystem-streaming.js +14 -14
  28. package/dist/examples/bci/bci-filesystem.js +17 -13
  29. package/dist/examples/cache-management.js +38 -38
  30. package/dist/examples/cancel-by-request-id.js +9 -9
  31. package/dist/examples/completion-events.js +20 -14
  32. package/dist/examples/completion-stop-reason.js +13 -7
  33. package/dist/examples/concurrent-completion-collision.js +7 -7
  34. package/dist/examples/config-reload.js +7 -8
  35. package/dist/examples/default-config-usage.js +4 -4
  36. package/dist/examples/delegated-inference/composite.js +14 -10
  37. package/dist/examples/delegated-inference/consumer-profiled.js +13 -13
  38. package/dist/examples/delegated-inference/consumer.js +16 -12
  39. package/dist/examples/delegated-inference/provider.js +12 -12
  40. package/dist/examples/diffusion-esrgan-upscale.js +19 -15
  41. package/dist/examples/diffusion-flux2-klein-img2img.js +16 -11
  42. package/dist/examples/diffusion-flux2-klein.js +14 -9
  43. package/dist/examples/diffusion-img2img.js +9 -10
  44. package/dist/examples/diffusion-img2vid.js +17 -12
  45. package/dist/examples/diffusion-simple.js +1 -1
  46. package/dist/examples/diffusion-txt2img.js +14 -9
  47. package/dist/examples/diffusion-txt2vid.js +15 -10
  48. package/dist/examples/download-with-blind-relays.js +16 -21
  49. package/dist/examples/download-with-cancel.js +15 -16
  50. package/dist/examples/embed-multi-gpu.js +3 -4
  51. package/dist/examples/embed-p2p.js +11 -7
  52. package/dist/examples/finetune/llamacpp-finetune.js +7 -7
  53. package/dist/examples/kv-cache-cleanup.js +35 -35
  54. package/dist/examples/kv-cache-custom-key.js +24 -24
  55. package/dist/examples/kv-cache-example.js +9 -9
  56. package/dist/examples/llamacpp-cache.js +18 -14
  57. package/dist/examples/llamacpp-dynamic-tools.js +15 -9
  58. package/dist/examples/llamacpp-filesystem.js +15 -9
  59. package/dist/examples/llamacpp-http.js +12 -10
  60. package/dist/examples/llamacpp-multi-gpu.js +4 -4
  61. package/dist/examples/llamacpp-multimodal.js +13 -9
  62. package/dist/examples/llamacpp-p2p.js +8 -4
  63. package/dist/examples/llamacpp-sharded.js +5 -5
  64. package/dist/examples/llamacpp-structured-output.js +15 -9
  65. package/dist/examples/logging-file-transport.js +3 -3
  66. package/dist/examples/logging-global.d.ts +2 -0
  67. package/dist/examples/logging-global.js +34 -0
  68. package/dist/examples/logging-streaming.js +7 -10
  69. package/dist/examples/mcp-websearch.js +24 -18
  70. package/dist/examples/multi-model-demo.js +17 -11
  71. package/dist/examples/ocr-fasttext.js +14 -16
  72. package/dist/examples/parallel-download.js +7 -7
  73. package/dist/examples/plugins.js +40 -28
  74. package/dist/examples/profiling/basic.js +16 -10
  75. package/dist/examples/profiling/per-call.js +17 -11
  76. package/dist/examples/quickstart.bare.d.ts +2 -0
  77. package/dist/examples/quickstart.bare.js +18 -0
  78. package/dist/examples/quickstart.js +10 -3
  79. package/dist/examples/rag/rag-chromadb.js +18 -14
  80. package/dist/examples/rag/rag-hyperdb/cancellation.js +17 -14
  81. package/dist/examples/rag/rag-hyperdb/ingest.js +14 -12
  82. package/dist/examples/rag/rag-hyperdb/pipeline.js +19 -17
  83. package/dist/examples/rag/rag-hyperdb/workspaces.js +31 -27
  84. package/dist/examples/rag/rag-lancedb.js +12 -8
  85. package/dist/examples/rag/rag-sqlite.js +11 -7
  86. package/dist/examples/registry-query.js +17 -17
  87. package/dist/examples/seed-p2p.js +10 -6
  88. package/dist/examples/suspend-resume.js +20 -16
  89. package/dist/examples/tools/llamacpp-native-tools.js +24 -18
  90. package/dist/examples/tools/llamacpp-tools-gemma4.js +14 -8
  91. package/dist/examples/tools/llamacpp-tools-harmony-multiturn.js +20 -14
  92. package/dist/examples/tools/llamacpp-tools-pythonic.js +14 -8
  93. package/dist/examples/tools/llamacpp-tools-qwen35.js +14 -8
  94. package/dist/examples/transcription/parakeet-ctc-filesystem.js +12 -9
  95. package/dist/examples/transcription/parakeet-microphone-record.js +15 -9
  96. package/dist/examples/transcription/parakeet-microphone-stream.js +15 -9
  97. package/dist/examples/transcription/parakeet-sortformer-streaming.js +12 -6
  98. package/dist/examples/transcription/parakeet-sortformer.js +3 -5
  99. package/dist/examples/transcription/parakeet-tdt-filesystem.js +13 -10
  100. package/dist/examples/transcription/whispercpp-filesystem-streaming.js +16 -17
  101. package/dist/examples/transcription/whispercpp-filesystem.js +14 -10
  102. package/dist/examples/transcription/whispercpp-microphone-conversation.js +10 -10
  103. package/dist/examples/transcription/whispercpp-microphone-record.js +6 -6
  104. package/dist/examples/transcription/whispercpp-prompt.js +14 -10
  105. package/dist/examples/translation/translation-bergamot-batch.js +11 -7
  106. package/dist/examples/translation/translation-bergamot-pivot.js +13 -9
  107. package/dist/examples/translation/translation-bergamot.js +8 -4
  108. package/dist/examples/translation/translation-indic.js +8 -4
  109. package/dist/examples/translation/translation-llm-afriquegemma.js +7 -3
  110. package/dist/examples/translation/translation-llm-context.js +8 -4
  111. package/dist/examples/translation/translation-llm.js +7 -3
  112. package/dist/examples/translation/translation-stream.js +4 -4
  113. package/dist/examples/tts/chatterbox.js +19 -11
  114. package/dist/examples/tts/llm-to-tts-streaming.js +27 -14
  115. package/dist/examples/tts/supertonic-multilingual.js +18 -14
  116. package/dist/examples/tts/supertonic.js +19 -15
  117. package/dist/examples/tts/utils.js +1 -1
  118. package/dist/examples/vla-pi05.js +18 -14
  119. package/dist/examples/vla-smolvla.js +18 -14
  120. package/dist/examples/voice-assistant/voice-assistant.js +12 -12
  121. package/dist/index.d.ts +3 -4
  122. package/dist/index.js +3 -4
  123. package/dist/logging/client-logger.js +2 -1
  124. package/dist/logging/index.d.ts +1 -1
  125. package/dist/logging/index.js +1 -1
  126. package/dist/logging/namespaces.d.ts +1 -0
  127. package/dist/logging/namespaces.js +2 -0
  128. package/dist/logging/server-logger.js +1 -4
  129. package/dist/logging/utils.js +6 -1
  130. package/dist/models/registry/index.d.ts +17 -0
  131. package/dist/models/registry/index.js +20 -1
  132. package/dist/models/registry/models.d.ts +1062 -38
  133. package/dist/models/registry/models.js +3546 -2490
  134. package/dist/pear/pre.js +2 -2
  135. package/dist/schemas/bci-config.d.ts +37 -0
  136. package/dist/schemas/bci-config.js +2 -0
  137. package/dist/schemas/bci.d.ts +2 -0
  138. package/dist/schemas/common.d.ts +171 -94
  139. package/dist/schemas/completion-stream.d.ts +6 -3
  140. package/dist/schemas/completion-stream.js +10 -2
  141. package/dist/schemas/download-asset.d.ts +5 -5
  142. package/dist/schemas/engine-addon-map.d.ts +1 -1
  143. package/dist/schemas/engine-addon-map.js +6 -3
  144. package/dist/schemas/error.js +6 -1
  145. package/dist/schemas/llamacpp-config.d.ts +27 -8
  146. package/dist/schemas/llamacpp-config.js +19 -2
  147. package/dist/schemas/load-model.d.ts +4170 -570
  148. package/dist/schemas/load-model.js +85 -26
  149. package/dist/schemas/logging-stream.js +7 -1
  150. package/dist/schemas/model-src-utils.d.ts +6 -6
  151. package/dist/schemas/model-types.d.ts +14 -15
  152. package/dist/schemas/model-types.js +10 -11
  153. package/dist/schemas/ocr.d.ts +10 -8
  154. package/dist/schemas/ocr.js +8 -6
  155. package/dist/schemas/plugin.d.ts +6 -5
  156. package/dist/schemas/plugin.js +4 -4
  157. package/dist/schemas/rag.d.ts +4 -4
  158. package/dist/schemas/registry.d.ts +8 -8
  159. package/dist/schemas/registry.js +1 -1
  160. package/dist/schemas/reload-config.d.ts +6 -6
  161. package/dist/schemas/sdcpp-config.d.ts +12 -12
  162. package/dist/schemas/sdk-config.d.ts +171 -117
  163. package/dist/schemas/sdk-config.js +5 -5
  164. package/dist/schemas/sdk-errors-client.d.ts +1 -0
  165. package/dist/schemas/sdk-errors-client.js +7 -2
  166. package/dist/schemas/text-to-speech.d.ts +157 -13
  167. package/dist/schemas/text-to-speech.js +70 -7
  168. package/dist/schemas/transcription-config.d.ts +5 -1
  169. package/dist/schemas/transcription-config.js +2 -0
  170. package/dist/schemas/transcription.d.ts +3 -0
  171. package/dist/schemas/transcription.js +2 -1
  172. package/dist/schemas/translate.d.ts +6 -6
  173. package/dist/schemas/translate.js +2 -3
  174. package/dist/schemas/translation-config.d.ts +20 -20
  175. package/dist/server/bare/hyperswarm.js +3 -3
  176. package/dist/server/bare/ops/transcribe.js +46 -15
  177. package/dist/server/bare/plugins/bci-whispercpp-transcription/args.d.ts +3 -0
  178. package/dist/server/bare/plugins/bci-whispercpp-transcription/args.js +13 -0
  179. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.d.ts +41 -0
  180. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.js +6 -11
  181. package/dist/server/bare/plugins/bci-whispercpp-transcription/resolve-config.d.ts +75 -0
  182. package/dist/server/bare/plugins/bci-whispercpp-transcription/resolve-config.js +11 -0
  183. package/dist/server/bare/plugins/{onnx-ocr → ggml-ocr}/plugin.d.ts +16 -10
  184. package/dist/server/bare/plugins/{onnx-ocr → ggml-ocr}/plugin.js +25 -18
  185. package/dist/server/bare/plugins/index.d.ts +1 -1
  186. package/dist/server/bare/plugins/index.js +1 -1
  187. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.d.ts +10 -0
  188. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stats.js +29 -0
  189. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +4 -22
  190. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +15 -7
  191. package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +7 -7
  192. package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +2 -0
  193. package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +10 -10
  194. package/dist/server/bare/plugins/tts-ggml/plugin.d.ts +14 -8
  195. package/dist/server/bare/plugins/tts-ggml/plugin.js +12 -0
  196. package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +3 -1
  197. package/dist/server/bare/registry/logging-stream-registry.d.ts +4 -2
  198. package/dist/server/bare/registry/logging-stream-registry.js +12 -7
  199. package/dist/server/bare/registry/model-config-utils.js +2 -2
  200. package/dist/server/bare/registry/registry-client.js +1 -1
  201. package/dist/server/bare/runtime-lifecycle.d.ts +9 -0
  202. package/dist/server/bare/runtime-lifecycle.js +24 -0
  203. package/dist/server/bare/types/addon-responses.d.ts +1 -0
  204. package/dist/server/env.js +7 -10
  205. package/dist/server/plugins/registry.js +2 -4
  206. package/dist/server/rpc/handlers/load-model/handler.js +2 -4
  207. package/dist/server/rpc/handlers/load-model/http.js +253 -51
  208. package/dist/server/rpc/handlers/logging-stream.js +4 -2
  209. package/dist/server/rpc/handlers/plugin-invoke.js +5 -16
  210. package/dist/server/utils/completion-normalizer.js +13 -4
  211. package/dist/server/utils/worker-lock.js +5 -5
  212. package/dist/server/worker-core.d.ts +1 -1
  213. package/dist/server/worker-core.js +28 -18
  214. package/dist/utils/errors-client.d.ts +3 -0
  215. package/dist/utils/errors-client.js +5 -0
  216. package/dist/utils/errors-server.d.ts +2 -0
  217. package/dist/utils/errors-server.js +3 -0
  218. package/dist/utils/zod-error.d.ts +3 -0
  219. package/dist/utils/zod-error.js +4 -0
  220. package/package.json +21 -24
  221. package/dist/polyfills/bare-globals.d.ts +0 -2
  222. package/dist/polyfills/bare-globals.js +0 -4
  223. package/dist/polyfills/bare-globals.noop.d.ts +0 -2
  224. package/dist/polyfills/bare-globals.noop.js +0 -1
  225. /package/dist/server/bare/plugins/{onnx-ocr → ggml-ocr}/ops/ocr-stream.d.ts +0 -0
  226. /package/dist/server/bare/plugins/{onnx-ocr → ggml-ocr}/ops/ocr-stream.js +0 -0
@@ -11,7 +11,7 @@ const logger = getServerLogger();
11
11
  // fd-lock issue a blocking flock(LOCK_EX) on a libuv worker thread. The
12
12
  // blocking variant cannot be cancelled from JS, so if another SDK process
13
13
  // holds the lock indefinitely it leaves a pending native handle that
14
- // prevents process.exit() from terminating the worker — see QVAC-18197.
14
+ // prevents Bare.exit() from terminating the worker — see QVAC-18197.
15
15
  //
16
16
  // With tryLock + bounded retries we get the same "tolerate transient locks
17
17
  // during another SDK's startup/shutdown" property #1480 wanted, but every
@@ -12,6 +12,15 @@ interface ResourceMeta {
12
12
  label: string;
13
13
  createdAt: number;
14
14
  }
15
+ type ResumeListener = () => void;
16
+ /**
17
+ * Register a callback fired after the runtime returns to "active". Lets in-flight
18
+ * work (e.g. an HTTP download whose socket died while backgrounded) recover the
19
+ * instant `resume()` runs instead of waiting for a timeout. Returns an
20
+ * unregister function — callers MUST call it when their work finishes so the
21
+ * listener doesn't leak into the next operation.
22
+ */
23
+ export declare function onResume(listener: ResumeListener): () => void;
15
24
  export declare function registerSwarm(swarm: SuspendableSwarm, meta: ResourceMeta): void;
16
25
  export declare function unregisterSwarm(swarm: SuspendableSwarm): void;
17
26
  export declare function registerCorestore(store: SuspendableStore, meta: ResourceMeta): void;
@@ -8,8 +8,30 @@ const LIFECYCLE_ALLOWED_TYPES = new Set([
8
8
  ]);
9
9
  const swarms = new Map();
10
10
  const stores = new Map();
11
+ const resumeListeners = new Set();
11
12
  let state = "active";
12
13
  let transitionPromise = null;
14
+ /**
15
+ * Register a callback fired after the runtime returns to "active". Lets in-flight
16
+ * work (e.g. an HTTP download whose socket died while backgrounded) recover the
17
+ * instant `resume()` runs instead of waiting for a timeout. Returns an
18
+ * unregister function — callers MUST call it when their work finishes so the
19
+ * listener doesn't leak into the next operation.
20
+ */
21
+ export function onResume(listener) {
22
+ resumeListeners.add(listener);
23
+ return () => resumeListeners.delete(listener);
24
+ }
25
+ function notifyResume() {
26
+ for (const listener of Array.from(resumeListeners)) {
27
+ try {
28
+ listener();
29
+ }
30
+ catch (error) {
31
+ logger.error("Lifecycle: onResume listener threw:", error);
32
+ }
33
+ }
34
+ }
13
35
  export function registerSwarm(swarm, meta) {
14
36
  swarms.set(swarm, meta);
15
37
  logger.debug(`Lifecycle: registered swarm [${meta.label}]`);
@@ -46,6 +68,7 @@ export function getRegisteredResourceCounts() {
46
68
  export function resetLifecycleState() {
47
69
  swarms.clear();
48
70
  stores.clear();
71
+ resumeListeners.clear();
49
72
  state = "active";
50
73
  transitionPromise = null;
51
74
  }
@@ -130,6 +153,7 @@ export async function resumeRuntime() {
130
153
  .then(() => {
131
154
  state = "active";
132
155
  logger.info("▶️ Runtime resumed");
156
+ notifyResume();
133
157
  })
134
158
  .catch((error) => {
135
159
  state = "suspended";
@@ -56,6 +56,7 @@ export interface TranscribeStats {
56
56
  melSpecMs?: number;
57
57
  backendDevice?: number;
58
58
  backendId?: number;
59
+ gpuUnsupported?: number;
59
60
  gpuMemTotalMb?: number;
60
61
  gpuMemFreeMb?: number;
61
62
  }
@@ -1,4 +1,4 @@
1
- import process from "bare-process";
1
+ import env from "bare-env";
2
2
  import { isBareKit } from "which-runtime";
3
3
  import { z } from "zod";
4
4
  const envSchema = z.object({
@@ -13,21 +13,18 @@ let validatedEnv = null;
13
13
  export function initEnv() {
14
14
  const defaultHomeDir =
15
15
  // Snap's HOME can be revision-scoped; SNAP_USER_COMMON is stable.
16
- process.env["SNAP_USER_COMMON"] ??
17
- process.env["HOME"] ??
18
- process.env["USERPROFILE"] ??
19
- "/tmp";
16
+ env["SNAP_USER_COMMON"] ?? env["HOME"] ?? env["USERPROFILE"] ?? "/tmp";
20
17
  let envConfig = {
21
18
  HOME_DIR: defaultHomeDir,
22
19
  };
23
20
  let hasRPCConfig = false;
24
- if (isBareKit && process.argv[0]) {
25
- envConfig["HOME_DIR"] = process.argv[0];
21
+ if (isBareKit && Bare.argv[0]) {
22
+ envConfig["HOME_DIR"] = Bare.argv[0];
26
23
  }
27
24
  // Try to parse any argument as JSON config (fail gracefully)
28
- if (process.argv[2]) {
25
+ if (Bare.argv[2]) {
29
26
  try {
30
- const rpcArgs = JSON.parse(process.argv[2]);
27
+ const rpcArgs = JSON.parse(Bare.argv[2]);
31
28
  envConfig = { ...envConfig, ...rpcArgs };
32
29
  hasRPCConfig = true;
33
30
  }
@@ -47,7 +44,7 @@ export function getEnv() {
47
44
  initEnv();
48
45
  }
49
46
  return {
50
- ...process.env,
47
+ ...env,
51
48
  ...validatedEnv,
52
49
  };
53
50
  }
@@ -2,6 +2,7 @@ import { pluginDefinitionRuntimeSchema, } from "../../schemas/plugin.js";
2
2
  import { isModelTypeAlias } from "../../schemas/index.js";
3
3
  import { PluginAlreadyRegisteredError, PluginDefinitionInvalidError, PluginLoggingInvalidError, PluginModelTypeReservedError, } from "../../utils/errors-server.js";
4
4
  import { createAddonLoggerCallback } from "../../logging/addon.js";
5
+ import { formatZodError } from "../../utils/zod-error.js";
5
6
  const plugins = new Map();
6
7
  function getModelTypeForError(plugin) {
7
8
  if (!plugin || typeof plugin !== "object")
@@ -17,10 +18,7 @@ function validatePluginDefinition(plugin) {
17
18
  const result = pluginDefinitionRuntimeSchema.safeParse(plugin);
18
19
  if (result.success)
19
20
  return;
20
- const details = result.error.issues
21
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
22
- .join(", ");
23
- throw new PluginDefinitionInvalidError(getModelTypeForError(plugin), details);
21
+ throw new PluginDefinitionInvalidError(getModelTypeForError(plugin), formatZodError(result.error));
24
22
  }
25
23
  export function registerPlugin(plugin) {
26
24
  validatePluginDefinition(plugin);
@@ -59,6 +59,7 @@ import { generateShortHash, canonicalConfigString, transformConfigForReload, } f
59
59
  import { buildDownloadProfilingFields } from "./types.js";
60
60
  import { ConfigReloadNotSupportedError, InferenceCancelledError, ModelTypeMismatchError, ModelIsDelegatedError, ModelNotFoundError, ModelLoadFailedError, PluginLoadConfigValidationFailedError, PluginNotFoundError, } from "../../../../utils/errors-server.js";
61
61
  import { getServerLogger } from "../../../../logging/index.js";
62
+ import { formatZodError } from "../../../../utils/zod-error.js";
62
63
  import { getPlugin } from "../../../plugins/index.js";
63
64
  import { getRequestRegistry, withRequestContext, } from "../../../bare/runtime/index.js";
64
65
  import { generateServerRequestId } from "../../../bare/runtime/request-id.js";
@@ -96,10 +97,7 @@ export async function handleLoadModel(request, progressCallback) {
96
97
  let resolvedModelConfig = (request.modelConfig ?? {});
97
98
  const parseResult = plugin.loadConfigSchema.safeParse(resolvedModelConfig);
98
99
  if (!parseResult.success) {
99
- const details = parseResult.error.issues
100
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
101
- .join(", ");
102
- throw new PluginLoadConfigValidationFailedError(canonicalModelType, details);
100
+ throw new PluginLoadConfigValidationFailedError(canonicalModelType, formatZodError(parseResult.error));
103
101
  }
104
102
  resolvedModelConfig = parseResult.data;
105
103
  const totalLoadStart = profilingEnabled ? nowMs() : 0;
@@ -2,15 +2,21 @@ import fs, { promises as fsPromises } from "bare-fs";
2
2
  import path from "bare-path";
3
3
  import { Readable } from "bare-stream";
4
4
  import fetch, { Headers } from "bare-fetch";
5
+ import { AbortController } from "bare-abort-controller";
5
6
  import { withTimeout } from "../../../../utils/withTimeout.js";
6
7
  import { getModelsCacheDir, getShardedModelCacheDir, generateShortHash, detectShardedModel, parsePatternBasedShardUrl, extractTensorsFromShards, calculatePercentage, isArchiveUrl, sanitizePathComponent, extractAndValidateShardedArchive, validateShardedModelCache, checkAllShardsExist, generateShardFilenames, hasValidGGUFHeader, } from "../../../utils/index.js";
7
8
  import { getSDKConfig } from "../../../bare/registry/config-registry.js";
9
+ import { getLifecycleState, onResume } from "../../../bare/runtime-lifecycle.js";
8
10
  import { createHttpDownloadKey, startOrJoinDownload, applyJoinedDownloadStats, } from "./download-manager.js";
9
11
  import { DownloadCancelledError, HTTPError, NoResponseBodyError, PartialDownloadOfflineError, ResponseBodyNotReadableError, } from "../../../../utils/errors-server.js";
10
12
  import { getServerLogger } from "../../../../logging/index.js";
11
13
  const logger = getServerLogger();
12
14
  const DEFAULT_CONCURRENCY = 3;
13
15
  const DEFAULT_HTTP_CONNECTION_TIMEOUT_MS = 10_000;
16
+ // If no bytes arrive for this long mid-stream, treat the transfer as stalled
17
+ // (dead socket after a suspend or network drop that didn't surface an error)
18
+ // and abort so the caller can resume from the partial via a Range request.
19
+ const HTTP_STREAM_STALL_TIMEOUT_MS = 10_000;
14
20
  function extractFilenameFromUrl(url) {
15
21
  // Parse URL to get the filename from the path
16
22
  const urlParts = url.split("/");
@@ -180,72 +186,140 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
180
186
  (typeof body.pipe === "function" &&
181
187
  typeof body.on === "function");
182
188
  if (isReadable) {
183
- // Track progress by intercepting data events if possible
184
- body.on("data", (chunk) => {
185
- downloadedBytes += chunk.length;
186
- if (progressCallback) {
187
- progressCallback({
188
- type: "modelProgress",
189
- downloaded: downloadedBytes,
190
- total: totalBytes,
191
- percentage: calculatePercentage(downloadedBytes, totalBytes),
192
- downloadKey,
193
- });
194
- }
195
- });
196
- // Pipe directly to file
197
- body.pipe(writeStream);
198
189
  // Wait for download to complete
199
190
  await new Promise((resolve, reject) => {
200
- // Handle abort signal
191
+ let stallTimer;
192
+ const clearStall = () => {
193
+ if (stallTimer) {
194
+ clearTimeout(stallTimer);
195
+ stallTimer = undefined;
196
+ }
197
+ };
198
+ // Reset on every chunk; if it ever fires, the socket went quiet
199
+ // mid-stream (suspend/drop) — abort so the caller resumes via Range.
200
+ const armStall = () => {
201
+ clearStall();
202
+ stallTimer = setTimeout(() => {
203
+ body.destroy();
204
+ writeStream.destroy();
205
+ reject(new Error(`HTTP stream stalled: no data for ${HTTP_STREAM_STALL_TIMEOUT_MS}ms`));
206
+ }, HTTP_STREAM_STALL_TIMEOUT_MS);
207
+ };
201
208
  const abortHandler = () => {
202
- const error = new DownloadCancelledError();
209
+ clearStall();
203
210
  body.destroy();
204
211
  writeStream.destroy();
205
- reject(error);
212
+ reject(new DownloadCancelledError());
206
213
  };
207
214
  if (signal) {
215
+ if (signal.aborted) {
216
+ abortHandler();
217
+ return;
218
+ }
208
219
  signal.addEventListener("abort", abortHandler);
209
220
  }
221
+ // Track progress + keep the stall watchdog fed
222
+ body.on("data", (chunk) => {
223
+ armStall();
224
+ downloadedBytes += chunk.length;
225
+ if (progressCallback) {
226
+ progressCallback({
227
+ type: "modelProgress",
228
+ downloaded: downloadedBytes,
229
+ total: totalBytes,
230
+ percentage: calculatePercentage(downloadedBytes, totalBytes),
231
+ downloadKey,
232
+ });
233
+ }
234
+ });
235
+ // Pipe directly to file
236
+ body.pipe(writeStream);
237
+ armStall();
210
238
  writeStream.on("finish", () => {
239
+ clearStall();
211
240
  logger.info(`✅ Model downloaded successfully to ${modelPath}`);
212
241
  if (signal) {
213
242
  signal.removeEventListener("abort", abortHandler);
214
243
  }
215
244
  resolve(undefined);
216
245
  });
217
- writeStream.on("error", reject);
218
- body.on("error", reject);
246
+ writeStream.on("error", (error) => {
247
+ clearStall();
248
+ reject(error);
249
+ });
250
+ body.on("error", (error) => {
251
+ clearStall();
252
+ reject(error);
253
+ });
219
254
  });
220
255
  }
221
256
  else if (body[Symbol.asyncIterator]) {
222
- // Body is an async iterable (for await...of)
223
- for await (const chunk of body) {
224
- // Check if abort signal is triggered
225
- if (signal && signal.aborted) {
226
- writeStream.destroy();
227
- throw new DownloadCancelledError();
228
- }
229
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
230
- downloadedBytes += buffer.length;
231
- if (progressCallback) {
232
- progressCallback({
233
- type: "modelProgress",
234
- downloaded: downloadedBytes,
235
- total: totalBytes,
236
- percentage: calculatePercentage(downloadedBytes, totalBytes),
237
- downloadKey,
257
+ // Body is an async iterable. Drive the iterator manually so each pull can
258
+ // race a stall timeout: a dead socket after suspend/drop produces neither
259
+ // a chunk nor an error, so the timeout converts it into a retriable
260
+ // failure and we abandon the iterator (which cancels the stream).
261
+ const iterator = body[Symbol.asyncIterator]();
262
+ let stalled = false;
263
+ try {
264
+ for (;;) {
265
+ let stallTimer;
266
+ const nextPromise = iterator.next();
267
+ nextPromise.catch(() => { });
268
+ let step;
269
+ try {
270
+ step = await Promise.race([
271
+ nextPromise,
272
+ new Promise((_, reject) => {
273
+ stallTimer = setTimeout(() => {
274
+ stalled = true;
275
+ reject(new Error(`HTTP stream stalled: no data for ${HTTP_STREAM_STALL_TIMEOUT_MS}ms`));
276
+ }, HTTP_STREAM_STALL_TIMEOUT_MS);
277
+ }),
278
+ ]);
279
+ }
280
+ finally {
281
+ if (stallTimer)
282
+ clearTimeout(stallTimer);
283
+ }
284
+ if (step.done)
285
+ break;
286
+ if (signal?.aborted) {
287
+ writeStream.destroy();
288
+ throw new DownloadCancelledError();
289
+ }
290
+ const buffer = Buffer.isBuffer(step.value)
291
+ ? step.value
292
+ : Buffer.from(step.value);
293
+ downloadedBytes += buffer.length;
294
+ if (progressCallback) {
295
+ progressCallback({
296
+ type: "modelProgress",
297
+ downloaded: downloadedBytes,
298
+ total: totalBytes,
299
+ percentage: calculatePercentage(downloadedBytes, totalBytes),
300
+ downloadKey,
301
+ });
302
+ }
303
+ // Write chunk to file
304
+ await new Promise((resolve, reject) => {
305
+ writeStream.write(buffer, (err) => {
306
+ if (err)
307
+ reject(new Error(err instanceof Error ? err.message : String(err)));
308
+ else
309
+ resolve();
310
+ });
238
311
  });
239
312
  }
240
- // Write chunk to file
241
- await new Promise((resolve, reject) => {
242
- writeStream.write(buffer, (err) => {
243
- if (err)
244
- reject(new Error(err instanceof Error ? err.message : String(err)));
245
- else
246
- resolve();
247
- });
248
- });
313
+ }
314
+ finally {
315
+ if (stalled) {
316
+ try {
317
+ await iterator.return?.();
318
+ }
319
+ catch {
320
+ /* best effort: free the dead socket */
321
+ }
322
+ }
249
323
  }
250
324
  // Close the write stream
251
325
  await new Promise((resolve, reject) => {
@@ -257,17 +331,45 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
257
331
  });
258
332
  }
259
333
  else {
260
- // Fallback: try to use getReader() if it's a ReadableStream
334
+ // Fallback: getReader() for a WHATWG ReadableStream (bare-fetch body).
261
335
  const readableStreamBody = body;
262
336
  const reader = readableStreamBody.getReader
263
337
  ? readableStreamBody.getReader()
264
338
  : null;
265
339
  if (reader) {
340
+ let cancelled = false;
266
341
  try {
267
- while (true) {
268
- const { done, value } = await reader.read();
342
+ for (;;) {
343
+ // Race each read against a stall timeout. A dead socket after a
344
+ // suspend/drop yields no data and no error; the timeout converts
345
+ // that into a retriable failure so the caller resumes via Range.
346
+ let stallTimer;
347
+ const readPromise = reader.read();
348
+ // Avoid an unhandled rejection if the read loses the race.
349
+ readPromise.catch(() => { });
350
+ let result;
351
+ try {
352
+ result = await Promise.race([
353
+ readPromise,
354
+ new Promise((_, reject) => {
355
+ stallTimer = setTimeout(() => {
356
+ cancelled = true;
357
+ reject(new Error(`HTTP stream stalled: no data for ${HTTP_STREAM_STALL_TIMEOUT_MS}ms`));
358
+ }, HTTP_STREAM_STALL_TIMEOUT_MS);
359
+ }),
360
+ ]);
361
+ }
362
+ finally {
363
+ if (stallTimer)
364
+ clearTimeout(stallTimer);
365
+ }
366
+ const { done, value } = result;
269
367
  if (done)
270
368
  break;
369
+ if (signal?.aborted) {
370
+ cancelled = true;
371
+ throw new DownloadCancelledError();
372
+ }
271
373
  const buffer = Buffer.from(value);
272
374
  downloadedBytes += buffer.length;
273
375
  if (progressCallback) {
@@ -291,6 +393,14 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
291
393
  }
292
394
  }
293
395
  finally {
396
+ if (cancelled) {
397
+ try {
398
+ await reader.cancel();
399
+ }
400
+ catch {
401
+ /* best effort: free the dead socket */
402
+ }
403
+ }
294
404
  reader.releaseLock();
295
405
  }
296
406
  // Close the write stream
@@ -313,6 +423,98 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
313
423
  throw error instanceof Error ? error : new Error(String(error));
314
424
  }
315
425
  }
426
+ const DEFAULT_HTTP_DOWNLOAD_MAX_RETRIES = 5;
427
+ const HTTP_RETRY_BASE_DELAY_MS = 500;
428
+ const LIFECYCLE_WAIT_POLL_MS = 200;
429
+ const LIFECYCLE_WAIT_MAX_MS = 5 * 60_000;
430
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
431
+ /**
432
+ * A mid-stream socket/body error, a connection failure (HTTP status 0, no
433
+ * response), or our own resume-triggered abort is recoverable by resuming from
434
+ * the partial. A real HTTP status (4xx/5xx) or a missing/unreadable body is not.
435
+ *
436
+ * `DownloadCancelledError` is intentionally NOT excluded here: this is only
437
+ * reached after the caller's consumer-cancel gate, so a cancellation at this
438
+ * point is our per-attempt abort (resume interrupt), which must be retried.
439
+ */
440
+ function isResumableTransferError(error) {
441
+ if (error instanceof NoResponseBodyError ||
442
+ error instanceof ResponseBodyNotReadableError ||
443
+ error instanceof PartialDownloadOfflineError) {
444
+ return false;
445
+ }
446
+ if (error instanceof HTTPError)
447
+ return error.httpStatus === 0;
448
+ return error instanceof Error;
449
+ }
450
+ /**
451
+ * Block until the runtime is active again before retrying. After a suspend the
452
+ * socket is dead and the process can't service the transfer; resuming the fetch
453
+ * only makes sense once `resume()` has run. Bounded and abort-aware.
454
+ */
455
+ async function waitForLifecycleActive(signal) {
456
+ if (getLifecycleState() === "active")
457
+ return;
458
+ const start = Date.now();
459
+ while (getLifecycleState() !== "active") {
460
+ if (signal?.aborted)
461
+ throw new DownloadCancelledError();
462
+ if (Date.now() - start > LIFECYCLE_WAIT_MAX_MS)
463
+ return;
464
+ await sleep(LIFECYCLE_WAIT_POLL_MS);
465
+ }
466
+ }
467
+ /**
468
+ * Run `performHttpDownload` with bounded retry. On a recoverable interruption
469
+ * (mid-stream socket drop, or a dead socket after suspend/network loss) it
470
+ * waits for the runtime to be active and re-issues the request, which resumes
471
+ * from the on-disk partial via a Range header. No consumer-side babysitting.
472
+ *
473
+ * Each attempt runs on its own AbortController that is aborted either by the
474
+ * consumer `signal` (a real cancel — terminal) or by `resume()` (a proactive
475
+ * recovery — the in-flight socket is dead after a background, so abort and
476
+ * range-resume immediately instead of waiting for the stall watchdog).
477
+ */
478
+ async function performHttpDownloadWithResume(url, modelPath, downloadKey, progressCallback, signal) {
479
+ const maxRetries = DEFAULT_HTTP_DOWNLOAD_MAX_RETRIES;
480
+ let attempt = 0;
481
+ for (;;) {
482
+ const attemptController = new AbortController();
483
+ const forwardCancel = () => attemptController.abort();
484
+ if (signal) {
485
+ if (signal.aborted)
486
+ attemptController.abort();
487
+ else
488
+ signal.addEventListener("abort", forwardCancel, { once: true });
489
+ }
490
+ // resume() → abort this attempt so it range-resumes now, not after the stall.
491
+ const offResume = onResume(() => attemptController.abort());
492
+ try {
493
+ await performHttpDownload(url, modelPath, downloadKey, progressCallback, attemptController.signal);
494
+ return;
495
+ }
496
+ catch (error) {
497
+ // A real consumer cancel is terminal; anything else (resume abort, stall,
498
+ // network error) is recoverable from the partial.
499
+ if (signal?.aborted) {
500
+ throw error instanceof DownloadCancelledError
501
+ ? error
502
+ : new DownloadCancelledError();
503
+ }
504
+ attempt++;
505
+ if (attempt > maxRetries || !isResumableTransferError(error))
506
+ throw error;
507
+ logger.warn(`⚠️ HTTP download interrupted (attempt ${attempt}/${maxRetries}), resuming from partial: ${error instanceof Error ? error.message : String(error)}`);
508
+ await waitForLifecycleActive(signal);
509
+ await sleep(HTTP_RETRY_BASE_DELAY_MS * attempt);
510
+ }
511
+ finally {
512
+ if (signal)
513
+ signal.removeEventListener("abort", forwardCancel);
514
+ offResume();
515
+ }
516
+ }
517
+ }
316
518
  export async function downloadModelFromHttp(url, progressCallback, hooks) {
317
519
  const filename = extractFilenameFromUrl(url);
318
520
  if (isArchiveUrl(url)) {
@@ -355,7 +557,7 @@ export async function downloadModelFromHttp(url, progressCallback, hooks) {
355
557
  // Download the file
356
558
  hooks?.markCacheMiss?.();
357
559
  ctx.setCacheHit(false);
358
- await performHttpDownload(url, modelPath, downloadKey, ctx.broadcastProgress, ctx.signal);
560
+ await performHttpDownloadWithResume(url, modelPath, downloadKey, ctx.broadcastProgress, ctx.signal);
359
561
  try {
360
562
  const stats = await fsPromises.stat(modelPath);
361
563
  ctx.broadcastProgress({
@@ -576,7 +778,7 @@ async function downloadShardedModelFromArchive(archiveUrl, progressCallback, hoo
576
778
  throw error;
577
779
  }
578
780
  async function downloadAndExtractArchive() {
579
- await performHttpDownload(archiveUrl, archivePath, downloadKey, ctx.broadcastProgress, ctx.signal);
781
+ await performHttpDownloadWithResume(archiveUrl, archivePath, downloadKey, ctx.broadcastProgress, ctx.signal);
580
782
  logger.info(`✅ Archive downloaded, extracting to: ${extractDir}`);
581
783
  const firstShardPath = await extractAndValidateShardedArchive(archivePath, extractDir, ctx.signal);
582
784
  try {
@@ -605,7 +807,7 @@ async function downloadShardsWithConcurrency(shardsToDownload, allShards, concur
605
807
  const state = queue.shift();
606
808
  const downloadPromise = (async () => {
607
809
  logger.info(`📥 Downloading shard ${state.index + 1}: ${state.shard.filename}`);
608
- await performHttpDownload(state.shard.url, state.shardPath, downloadKey, (progress) => {
810
+ await performHttpDownloadWithResume(state.shard.url, state.shardPath, downloadKey, (progress) => {
609
811
  state.downloadedBytes = progress.downloaded;
610
812
  if (progressCallback) {
611
813
  const overallDownloaded = allShards.reduce((sum, s) => sum + s.downloadedBytes, 0);
@@ -3,10 +3,12 @@ export async function* handleLoggingStream(request) {
3
3
  const { id } = request;
4
4
  const logQueue = [];
5
5
  let pendingResolve = null;
6
- const streamHandler = (level, namespace, message) => {
6
+ const streamHandler = (level, namespace, message, sourceId) => {
7
7
  const logResponse = {
8
8
  type: "loggingStream",
9
- id,
9
+ // For the global SDK_ALL_LOG_ID stream `sourceId` is the real origin of the
10
+ // log; for a per-id stream it equals the subscription `id`.
11
+ id: sourceId,
10
12
  level: level,
11
13
  namespace,
12
14
  message,
@@ -3,6 +3,7 @@ import { getPlugin, getPluginHandler } from "../../plugins/index.js";
3
3
  import { profileReplyHandler, profileStreamHandler, } from "../profiling/index.js";
4
4
  import { PluginNotFoundError, PluginHandlerNotFoundError, PluginHandlerTypeMismatchError, PluginRequestValidationFailedError, PluginResponseValidationFailedError, ModelIsDelegatedError, ModelNotFoundError, } from "../../../utils/errors-server.js";
5
5
  import { getServerLogger } from "../../../logging/index.js";
6
+ import { formatZodError } from "../../../utils/zod-error.js";
6
7
  const logger = getServerLogger();
7
8
  function resolvePluginHandler(modelId, handlerName) {
8
9
  const modelEntry = getModelEntry(modelId);
@@ -34,18 +35,12 @@ export async function handlePluginInvoke(request) {
34
35
  }
35
36
  const parseResult = handlerDef.requestSchema.safeParse(params);
36
37
  if (!parseResult.success) {
37
- const details = parseResult.error.issues
38
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
39
- .join(", ");
40
- throw new PluginRequestValidationFailedError(handlerName, details);
38
+ throw new PluginRequestValidationFailedError(handlerName, formatZodError(parseResult.error));
41
39
  }
42
40
  const result = await handlerDef.handler(parseResult.data);
43
41
  const responseParseResult = handlerDef.responseSchema.safeParse(result);
44
42
  if (!responseParseResult.success) {
45
- const details = responseParseResult.error.issues
46
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
47
- .join(", ");
48
- throw new PluginResponseValidationFailedError(handlerName, details);
43
+ throw new PluginResponseValidationFailedError(handlerName, formatZodError(responseParseResult.error));
49
44
  }
50
45
  return {
51
46
  type: "pluginInvoke",
@@ -63,19 +58,13 @@ export async function* handlePluginInvokeStream(request) {
63
58
  }
64
59
  const parseResult = handlerDef.requestSchema.safeParse(params);
65
60
  if (!parseResult.success) {
66
- const details = parseResult.error.issues
67
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
68
- .join(", ");
69
- throw new PluginRequestValidationFailedError(handlerName, details);
61
+ throw new PluginRequestValidationFailedError(handlerName, formatZodError(parseResult.error));
70
62
  }
71
63
  const generator = handlerDef.handler(parseResult.data);
72
64
  for await (const chunk of generator) {
73
65
  const responseParseResult = handlerDef.responseSchema.safeParse(chunk);
74
66
  if (!responseParseResult.success) {
75
- const details = responseParseResult.error.issues
76
- .map((i) => `${String(i.path.join("."))}: ${i.message}`)
77
- .join(", ");
78
- throw new PluginResponseValidationFailedError(handlerName, details);
67
+ throw new PluginResponseValidationFailedError(handlerName, formatZodError(responseParseResult.error));
79
68
  }
80
69
  yield {
81
70
  type: "pluginInvokeStream",