@qvac/sdk 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/NOTICE +55 -1225
  2. package/README.md +12 -0
  3. package/dist/client/api/bci-transcribe.d.ts +57 -0
  4. package/dist/client/api/bci-transcribe.js +172 -0
  5. package/dist/client/api/get-model-info.d.ts +1 -1
  6. package/dist/client/api/index.d.ts +1 -0
  7. package/dist/client/api/index.js +1 -0
  8. package/dist/client/api/video.d.ts +26 -6
  9. package/dist/client/api/video.js +30 -7
  10. package/dist/client/api/vla.d.ts +11 -7
  11. package/dist/client/api/vla.js +11 -7
  12. package/dist/client/rpc/bare-client.js +2 -2
  13. package/dist/commands/bundle/constants.js +2 -0
  14. package/dist/examples/bci/bci-filesystem-streaming.d.ts +2 -0
  15. package/dist/examples/bci/bci-filesystem-streaming.js +77 -0
  16. package/dist/examples/bci/bci-filesystem.d.ts +2 -0
  17. package/dist/examples/bci/bci-filesystem.js +63 -0
  18. package/dist/examples/cache-management.js +0 -1
  19. package/dist/examples/cancel-by-request-id.js +0 -1
  20. package/dist/examples/classification/classify-image.js +1 -1
  21. package/dist/examples/completion-events.js +6 -2
  22. package/dist/examples/completion-stop-reason.d.ts +25 -0
  23. package/dist/examples/completion-stop-reason.js +65 -0
  24. package/dist/examples/concurrent-completion-collision.d.ts +2 -0
  25. package/dist/examples/concurrent-completion-collision.js +98 -0
  26. package/dist/examples/config-reload.js +1 -2
  27. package/dist/examples/default-config-usage.js +0 -1
  28. package/dist/examples/delegated-inference/composite.js +0 -1
  29. package/dist/examples/delegated-inference/consumer-profiled.js +0 -1
  30. package/dist/examples/delegated-inference/consumer.js +0 -1
  31. package/dist/examples/diffusion-esrgan-upscale.js +1 -3
  32. package/dist/examples/diffusion-flux2-klein-img2img.js +1 -1
  33. package/dist/examples/diffusion-flux2-klein.js +2 -2
  34. package/dist/examples/diffusion-img2img.js +1 -1
  35. package/dist/examples/diffusion-img2vid.d.ts +2 -0
  36. package/dist/examples/diffusion-img2vid.js +78 -0
  37. package/dist/examples/diffusion-simple.js +1 -1
  38. package/dist/examples/diffusion-txt2img.js +8 -3
  39. package/dist/examples/diffusion-txt2vid.js +2 -3
  40. package/dist/examples/download-with-blind-relays.js +0 -1
  41. package/dist/examples/embed-multi-gpu.js +1 -2
  42. package/dist/examples/embed-p2p.js +4 -2
  43. package/dist/examples/finetune/llamacpp-finetune.js +5 -3
  44. package/dist/examples/kv-cache-cleanup.js +0 -1
  45. package/dist/examples/kv-cache-custom-key.js +0 -1
  46. package/dist/examples/kv-cache-example.js +4 -2
  47. package/dist/examples/llamacpp-cache.js +0 -1
  48. package/dist/examples/llamacpp-dynamic-tools.js +4 -2
  49. package/dist/examples/llamacpp-filesystem.js +1 -1
  50. package/dist/examples/llamacpp-http.js +1 -1
  51. package/dist/examples/llamacpp-multi-gpu.js +2 -2
  52. package/dist/examples/llamacpp-multimodal.js +0 -1
  53. package/dist/examples/llamacpp-p2p.js +0 -1
  54. package/dist/examples/llamacpp-sharded.js +1 -1
  55. package/dist/examples/llamacpp-structured-output.js +0 -1
  56. package/dist/examples/logging-file-transport.js +0 -1
  57. package/dist/examples/logging-streaming.js +0 -2
  58. package/dist/examples/mcp-websearch.js +0 -1
  59. package/dist/examples/multi-model-demo.js +0 -2
  60. package/dist/examples/ocr-fasttext.js +0 -1
  61. package/dist/examples/plugins.js +1 -4
  62. package/dist/examples/profiling/basic.js +3 -2
  63. package/dist/examples/profiling/per-call.js +0 -1
  64. package/dist/examples/quickstart.js +0 -1
  65. package/dist/examples/rag/rag-chromadb.js +0 -1
  66. package/dist/examples/rag/rag-hyperdb/cancellation.js +0 -1
  67. package/dist/examples/rag/rag-hyperdb/ingest.js +0 -1
  68. package/dist/examples/rag/rag-hyperdb/pipeline.js +0 -1
  69. package/dist/examples/rag/rag-hyperdb/workspaces.js +0 -1
  70. package/dist/examples/rag/rag-lancedb.js +0 -1
  71. package/dist/examples/rag/rag-sqlite.js +0 -1
  72. package/dist/examples/suspend-resume.js +0 -1
  73. package/dist/examples/tools/llamacpp-native-tools.js +0 -1
  74. package/dist/examples/tools/llamacpp-tools-gemma4.js +1 -1
  75. package/dist/examples/tools/llamacpp-tools-pythonic.js +2 -2
  76. package/dist/examples/tools/llamacpp-tools-qwen35.js +1 -1
  77. package/dist/examples/transcription/parakeet-ctc-filesystem.js +1 -1
  78. package/dist/examples/transcription/parakeet-microphone-record.js +0 -1
  79. package/dist/examples/transcription/parakeet-microphone-stream.js +0 -1
  80. package/dist/examples/transcription/parakeet-sortformer.js +1 -2
  81. package/dist/examples/transcription/parakeet-tdt-filesystem.js +1 -1
  82. package/dist/examples/transcription/whispercpp-filesystem-streaming.js +10 -6
  83. package/dist/examples/transcription/whispercpp-filesystem.js +4 -2
  84. package/dist/examples/transcription/whispercpp-microphone-conversation.js +0 -1
  85. package/dist/examples/transcription/whispercpp-microphone-record.js +0 -1
  86. package/dist/examples/transcription/whispercpp-prompt.js +0 -1
  87. package/dist/examples/translation/translation-bergamot-batch.js +1 -2
  88. package/dist/examples/translation/translation-bergamot-pivot.js +3 -4
  89. package/dist/examples/translation/translation-bergamot.js +1 -2
  90. package/dist/examples/translation/translation-indic.js +1 -2
  91. package/dist/examples/translation/translation-llm-afriquegemma.js +3 -4
  92. package/dist/examples/translation/translation-llm-context.js +1 -2
  93. package/dist/examples/translation/translation-llm.js +2 -3
  94. package/dist/examples/translation/translation-stream.js +2 -3
  95. package/dist/examples/tts/chatterbox.js +1 -2
  96. package/dist/examples/tts/llm-to-tts-streaming.js +2 -4
  97. package/dist/examples/tts/supertonic-multilingual.js +1 -2
  98. package/dist/examples/tts/supertonic.js +1 -2
  99. package/dist/examples/vla-pi05.d.ts +2 -0
  100. package/dist/examples/vla-pi05.js +84 -0
  101. package/dist/examples/vla-smolvla.js +1 -1
  102. package/dist/examples/voice-assistant/voice-assistant.js +1 -4
  103. package/dist/index.d.ts +2 -2
  104. package/dist/index.js +9 -7
  105. package/dist/models/registry/models.d.ts +869 -186
  106. package/dist/models/registry/models.js +3160 -2453
  107. package/dist/models/update-models/companions.d.ts +7 -0
  108. package/dist/models/update-models/companions.js +56 -0
  109. package/dist/models/update-models/naming.js +10 -0
  110. package/dist/pear/pre.js +2 -0
  111. package/dist/schemas/bci-config.d.ts +36 -0
  112. package/dist/schemas/bci-config.js +56 -0
  113. package/dist/schemas/bci.d.ts +177 -0
  114. package/dist/schemas/bci.js +95 -0
  115. package/dist/schemas/common.d.ts +395 -51
  116. package/dist/schemas/common.js +5 -0
  117. package/dist/schemas/completion-event.d.ts +1 -0
  118. package/dist/schemas/download-asset.d.ts +7 -3
  119. package/dist/schemas/embed.d.ts +2 -0
  120. package/dist/schemas/embed.js +1 -0
  121. package/dist/schemas/engine-addon-map.d.ts +1 -0
  122. package/dist/schemas/engine-addon-map.js +4 -1
  123. package/dist/schemas/get-model-info.d.ts +2 -0
  124. package/dist/schemas/get-model-info.js +1 -0
  125. package/dist/schemas/index.d.ts +2 -0
  126. package/dist/schemas/index.js +2 -0
  127. package/dist/schemas/llamacpp-config.d.ts +6 -2
  128. package/dist/schemas/load-model.d.ts +1841 -267
  129. package/dist/schemas/load-model.js +34 -1
  130. package/dist/schemas/model-src-utils.d.ts +10 -2
  131. package/dist/schemas/model-types.d.ts +19 -1
  132. package/dist/schemas/model-types.js +11 -0
  133. package/dist/schemas/ocr.d.ts +2 -0
  134. package/dist/schemas/plugin.d.ts +13 -5
  135. package/dist/schemas/plugin.js +13 -4
  136. package/dist/schemas/registry.d.ts +18 -0
  137. package/dist/schemas/registry.js +2 -0
  138. package/dist/schemas/reload-config.d.ts +10 -2
  139. package/dist/schemas/sdcpp-config.d.ts +75 -8
  140. package/dist/schemas/sdcpp-config.js +56 -10
  141. package/dist/schemas/sdk-config.d.ts +342 -0
  142. package/dist/schemas/text-to-speech.d.ts +8 -0
  143. package/dist/schemas/transcription-config.d.ts +2 -0
  144. package/dist/schemas/transcription.d.ts +12 -0
  145. package/dist/schemas/transcription.js +8 -0
  146. package/dist/schemas/translation-config.d.ts +44 -16
  147. package/dist/schemas/translation-config.js +2 -0
  148. package/dist/schemas/vla.d.ts +14 -0
  149. package/dist/schemas/vla.js +26 -3
  150. package/dist/server/bare/delegate-connect-diagnostics.d.ts +9 -0
  151. package/dist/server/bare/delegate-connect-diagnostics.js +44 -0
  152. package/dist/server/bare/delegate-rpc-client.js +10 -12
  153. package/dist/server/bare/hyperswarm.js +1 -0
  154. package/dist/server/bare/ops/bci-transcribe.d.ts +13 -0
  155. package/dist/server/bare/ops/bci-transcribe.js +248 -0
  156. package/dist/server/bare/ops/embed.js +2 -1
  157. package/dist/server/bare/ops/kv-cache-utils.js +4 -3
  158. package/dist/server/bare/ops/transcribe.js +6 -2
  159. package/dist/server/bare/ops/translate.js +1 -1
  160. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.d.ts +130 -0
  161. package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.js +116 -0
  162. package/dist/server/bare/plugins/ggml-vla/ops/vla-run.js +4 -0
  163. package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +7 -0
  164. package/dist/server/bare/plugins/ggml-vla/plugin.js +1 -1
  165. package/dist/server/bare/plugins/index.d.ts +1 -0
  166. package/dist/server/bare/plugins/index.js +1 -0
  167. package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +22 -29
  168. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +2 -0
  169. package/dist/server/bare/plugins/llamacpp-completion/plugin.js +38 -7
  170. package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +1 -0
  171. package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +9 -1
  172. package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +14 -0
  173. package/dist/server/bare/plugins/onnx-ocr/plugin.d.ts +2 -0
  174. package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +8 -0
  175. package/dist/server/bare/plugins/sdcpp-generation/ops/video.js +7 -1
  176. package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +58 -4
  177. package/dist/server/bare/plugins/sdcpp-generation/plugin.js +9 -4
  178. package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +10 -0
  179. package/dist/server/bare/registry/model-config-utils.js +3 -0
  180. package/dist/server/bare/runtime/request-registry-singleton.js +11 -4
  181. package/dist/server/bare/runtime/request-registry.d.ts +60 -16
  182. package/dist/server/bare/runtime/request-registry.js +258 -15
  183. package/dist/server/bare/runtime/with-request-context.d.ts +1 -1
  184. package/dist/server/bare/runtime/with-request-context.js +1 -1
  185. package/dist/server/bare/types/addon-responses.d.ts +5 -0
  186. package/dist/server/rpc/handler-registry.js +4 -0
  187. package/dist/server/rpc/handlers/bci-transcribe-stream.d.ts +3 -0
  188. package/dist/server/rpc/handlers/bci-transcribe-stream.js +4 -0
  189. package/dist/server/rpc/handlers/bci-transcribe.d.ts +3 -0
  190. package/dist/server/rpc/handlers/bci-transcribe.js +4 -0
  191. package/dist/server/rpc/handlers/download-asset.js +1 -1
  192. package/dist/server/rpc/handlers/get-model-info.js +3 -3
  193. package/dist/server/rpc/handlers/index.d.ts +4 -0
  194. package/dist/server/rpc/handlers/index.js +4 -0
  195. package/dist/server/rpc/handlers/load-model/download-manager.d.ts +1 -1
  196. package/dist/server/rpc/handlers/load-model/handler.js +1 -1
  197. package/dist/server/rpc/handlers/load-model/http.js +6 -6
  198. package/dist/server/rpc/handlers/load-model/hyperdrive.js +1 -0
  199. package/dist/server/rpc/handlers/load-model/registry-companion-set.d.ts +1 -0
  200. package/dist/server/rpc/handlers/load-model/registry-companion-set.js +2 -2
  201. package/dist/server/rpc/handlers/load-model/registry.js +1 -0
  202. package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
  203. package/dist/server/rpc/handlers/rag.js +5 -5
  204. package/dist/server/utils/checksum.js +2 -1
  205. package/dist/server/utils/formatting.js +2 -1
  206. package/dist/server/utils/multi-gpu-mobile.d.ts +5 -0
  207. package/dist/server/utils/multi-gpu-mobile.js +10 -0
  208. package/dist/server/worker.js +2 -1
  209. package/dist/utils/aggregate-events.d.ts +2 -1
  210. package/dist/utils/aggregate-events.js +11 -3
  211. package/dist/utils/errors-server.d.ts +6 -4
  212. package/dist/utils/errors-server.js +6 -4
  213. package/electron-forge/index.cjs +820 -0
  214. package/package.json +68 -35
  215. package/dist/scripts/run-security-tests-bare.d.ts +0 -2
  216. package/dist/scripts/run-security-tests-bare.js +0 -78
@@ -24,10 +24,30 @@ import { getServerLogger } from "../../../logging/index.js";
24
24
  */
25
25
  const CANCEL_BEFORE_BEGIN_MAX_ENTRIES = 128;
26
26
  const CANCEL_BEFORE_BEGIN_TTL_MS = 30_000;
27
+ /**
28
+ * Default cap on waiters queued per `(kind, modelId)` when a policy uses
29
+ * `onOverflow: "queue"` without an explicit `maxQueueDepthPerModel`.
30
+ * Bounds memory so a misbehaving client firing a flood of same-model
31
+ * requests can't grow the queue without limit — the (depth + 1)th begin
32
+ * rejects with `RequestRejectedByPolicyError` instead of enqueuing.
33
+ */
34
+ const DEFAULT_MAX_QUEUE_DEPTH_PER_MODEL = 64;
27
35
  export function createRequestRegistry(options) {
28
36
  const entries = new Map();
29
37
  const policies = new Map();
30
38
  const logger = options?.logger ?? getServerLogger();
39
+ /**
40
+ * Per-`(kind, modelId)` admission semaphores. Keyed by
41
+ * `slotKey(kind, modelId)`. Absent until the first gated `begin(...)`
42
+ * for that key; removed again the moment the key is idle.
43
+ */
44
+ const keyStates = new Map();
45
+ /**
46
+ * Reverse index from a queued waiter's `requestId` to its slot so
47
+ * `cancel({ requestId })` can pull a still-queued request out of the
48
+ * FIFO in O(1) without scanning every `KeyState`.
49
+ */
50
+ const waitersById = new Map();
31
51
  function logLifecycle(event, ctx, durationMs) {
32
52
  const modelId = ctx.modelId !== undefined ? ctx.modelId : "-";
33
53
  const base = `[request-lifecycle] ${event} requestId=${ctx.requestId} kind=${ctx.kind} modelId=${modelId} state=${ctx.state}`;
@@ -99,6 +119,124 @@ export function createRequestRegistry(options) {
99
119
  cancelledBeforeBegin.delete(requestId);
100
120
  return entry;
101
121
  }
122
+ function slotKey(kind, modelId) {
123
+ // NUL separator can't appear in a kind (closed union) or a modelId
124
+ // (sdkModelId is a generated handle), so the join is unambiguous.
125
+ return `${kind}\u0000${modelId}`;
126
+ }
127
+ /**
128
+ * Pull a waiter out of its queue and tidy up: clear its timeout, drop
129
+ * the reverse index, and delete the `KeyState` if that left it idle.
130
+ * Does NOT settle the waiter's promise — the caller decides whether the
131
+ * removal resolves (granted / graceful cancel) or rejects (timeout /
132
+ * teardown).
133
+ */
134
+ function removeWaiter(key, waiter) {
135
+ if (waiter.timer !== undefined) {
136
+ clearTimeout(waiter.timer);
137
+ waiter.timer = undefined;
138
+ }
139
+ waitersById.delete(waiter.requestId);
140
+ const st = keyStates.get(key);
141
+ if (!st)
142
+ return;
143
+ const i = st.waiters.indexOf(waiter);
144
+ if (i >= 0)
145
+ st.waiters.splice(i, 1);
146
+ if (st.active <= 0 && st.waiters.length === 0)
147
+ keyStates.delete(key);
148
+ }
149
+ /**
150
+ * Admission gate. Resolves to the slot key the caller must release on
151
+ * dispose, or `undefined` when the request is ungated (no policy, no
152
+ * `modelId`, an unbounded limit, or an already-aborted parent — which we
153
+ * let through so the normal abort path produces a clean cancelled
154
+ * outcome rather than queuing a doomed request).
155
+ *
156
+ * When the key is at capacity it either rejects (`onOverflow: "reject"`,
157
+ * queue-depth cap, or `queueTimeoutMs`) or enqueues a FIFO waiter and
158
+ * awaits it. The releaser hands a freed slot to the oldest waiter
159
+ * (`resolve(true)`), so `active` is never decremented and re-incremented
160
+ * across a hand-off — that's what guarantees FIFO fairness with no
161
+ * acquire/release race in the single-threaded event loop.
162
+ */
163
+ async function acquireSlot(opts) {
164
+ if (opts.modelId === undefined)
165
+ return { slotKey: undefined };
166
+ const policy = policies.get(opts.kind);
167
+ if (!policy || !Number.isFinite(policy.maxConcurrent)) {
168
+ return { slotKey: undefined };
169
+ }
170
+ // A parent (worker-shutdown) signal that's already aborted: don't
171
+ // queue behind live work that may never drain — let begin() proceed
172
+ // and abort immediately via the parentSignal path.
173
+ if (opts.parentSignal?.aborted)
174
+ return { slotKey: undefined };
175
+ const modelId = opts.modelId;
176
+ const key = slotKey(opts.kind, modelId);
177
+ let st = keyStates.get(key);
178
+ if (!st) {
179
+ st = { active: 0, waiters: [] };
180
+ keyStates.set(key, st);
181
+ }
182
+ if (st.active < policy.maxConcurrent) {
183
+ st.active++;
184
+ return { slotKey: key };
185
+ }
186
+ if (policy.onOverflow === "reject") {
187
+ throw new RequestRejectedByPolicyError(opts.requestId, opts.kind, modelId, `another ${opts.kind} request is already running on this model`);
188
+ }
189
+ if (st.waiters.length >= policy.maxQueueDepth) {
190
+ throw new RequestRejectedByPolicyError(opts.requestId, opts.kind, modelId, `request queue for this model is full (${st.waiters.length} waiting)`);
191
+ }
192
+ const queue = st;
193
+ const granted = await new Promise((resolve, reject) => {
194
+ const waiter = {
195
+ requestId: opts.requestId,
196
+ kind: opts.kind,
197
+ modelId,
198
+ enqueuedAt: Date.now(),
199
+ resolve,
200
+ reject,
201
+ timer: undefined,
202
+ };
203
+ if (policy.queueTimeoutMs !== undefined) {
204
+ const timeoutMs = policy.queueTimeoutMs;
205
+ waiter.timer = setTimeout(() => {
206
+ removeWaiter(key, waiter);
207
+ reject(new RequestRejectedByPolicyError(opts.requestId, opts.kind, modelId, `timed out waiting ${timeoutMs}ms for a slot on this model`));
208
+ }, timeoutMs);
209
+ }
210
+ queue.waiters.push(waiter);
211
+ waitersById.set(opts.requestId, { key, waiter });
212
+ });
213
+ return granted ? { slotKey: key } : { slotKey: undefined };
214
+ }
215
+ /**
216
+ * Hand a freed slot to the next FIFO waiter, or free it outright when
217
+ * none are queued. Called from `disposeEntry` after the scope has fully
218
+ * unwound, so the native context is genuinely free before the next
219
+ * same-model request is admitted.
220
+ */
221
+ function releaseSlot(key) {
222
+ const st = keyStates.get(key);
223
+ if (!st)
224
+ return;
225
+ const next = st.waiters.shift();
226
+ if (next) {
227
+ // Hand off: the waiter inherits the slot, so `active` is unchanged.
228
+ waitersById.delete(next.requestId);
229
+ if (next.timer !== undefined) {
230
+ clearTimeout(next.timer);
231
+ next.timer = undefined;
232
+ }
233
+ next.resolve(true);
234
+ return;
235
+ }
236
+ st.active--;
237
+ if (st.active <= 0 && st.waiters.length === 0)
238
+ keyStates.delete(key);
239
+ }
102
240
  function cancelEntry(entry, reason) {
103
241
  if (entry.controller.signal.aborted)
104
242
  return false;
@@ -116,23 +254,43 @@ export function createRequestRegistry(options) {
116
254
  // Pull the entry out before unwinding so observers (e.g. a `cancel(...)`
117
255
  // racing with dispose) don't see a half-disposed context.
118
256
  entries.delete(entry.ctx.requestId);
119
- await entry.scope[Symbol.asyncDispose]();
257
+ try {
258
+ await entry.scope[Symbol.asyncDispose]();
259
+ }
260
+ finally {
261
+ // Release the admission slot only after the scope has fully unwound:
262
+ // the shared llama.cpp context / addon job is torn down in scope
263
+ // cleanup, so the next queued same-model request must not be admitted
264
+ // (and start decoding) until this one has actually let go. `finally`
265
+ // guarantees the slot is freed even if a deferred cleanup throws,
266
+ // otherwise a throwing teardown would strand the queue forever.
267
+ if (entry.slotKey !== undefined)
268
+ releaseSlot(entry.slotKey);
269
+ }
120
270
  }
121
- function begin(opts) {
122
- if (entries.has(opts.requestId)) {
271
+ async function begin(opts) {
272
+ // A request id is reserved for its whole lifecycle, including the time
273
+ // it spends *queued* for an admission slot. `waitersById` holds the
274
+ // still-queued begins (they aren't in `entries` yet), so a duplicate id
275
+ // must be rejected against both maps — otherwise a second begin with the
276
+ // same id would enqueue behind the first and overwrite its `waitersById`
277
+ // index, leaving the original waiter unreachable by `cancel({ requestId })`.
278
+ if (entries.has(opts.requestId) || waitersById.has(opts.requestId)) {
123
279
  throw new RequestIdConflictError(opts.requestId);
124
280
  }
125
- // Admission control runs before allocation so a rejected begin
126
- // leaves no controller / scope behind.
127
- const policy = policies.get(opts.kind);
128
- if (policy && policy.oneAtATimePerModel && opts.modelId !== undefined) {
129
- for (const existing of entries.values()) {
130
- if (existing.ctx.kind !== opts.kind)
131
- continue;
132
- if (existing.ctx.modelId !== opts.modelId)
133
- continue;
134
- throw new RequestRejectedByPolicyError(opts.requestId, opts.kind, opts.modelId, `another ${opts.kind} request (${existing.ctx.requestId}) is already running on this model`);
135
- }
281
+ // Admission control runs before allocation so a rejected begin leaves
282
+ // no controller / scope behind. This may await: a gated `(kind,
283
+ // modelId)` at capacity queues the begin FIFO and resolves once a slot
284
+ // frees (or rejects on overflow / queue-depth cap / timeout).
285
+ const { slotKey } = await acquireSlot(opts);
286
+ // The only interleaving point in this otherwise synchronous body is the
287
+ // await above. A duplicate id could (astronomically unlikely) have
288
+ // landed while we were queued — re-check and hand the just-acquired
289
+ // slot back rather than stranding it on the conflicting throw.
290
+ if (entries.has(opts.requestId)) {
291
+ if (slotKey !== undefined)
292
+ releaseSlot(slotKey);
293
+ throw new RequestIdConflictError(opts.requestId);
136
294
  }
137
295
  const controller = new AbortController();
138
296
  const scope = createDisposableScope();
@@ -179,6 +337,7 @@ export function createRequestRegistry(options) {
179
337
  scope,
180
338
  detachParent,
181
339
  startedAt: Date.now(),
340
+ slotKey,
182
341
  };
183
342
  entries.set(opts.requestId, entry);
184
343
  logLifecycle("begin", ctx);
@@ -215,6 +374,18 @@ export function createRequestRegistry(options) {
215
374
  function list() {
216
375
  return Array.from(entries.values(), (e) => e.ctx);
217
376
  }
377
+ /**
378
+ * Cancel a still-queued waiter the same way the Stop-button race is
379
+ * handled: record a cancel-before-begin so its `begin(...)` — which
380
+ * resumes the instant we `resolve(false)` (without a slot) — observes the
381
+ * cancel and returns an already-aborted context. The request thus ends in
382
+ * a clean `cancelled` state rather than throwing.
383
+ */
384
+ function cancelQueuedWaiterGracefully(key, waiter, reason) {
385
+ recordCancelBeforeBegin(waiter.requestId, reason);
386
+ removeWaiter(key, waiter);
387
+ waiter.resolve(false);
388
+ }
218
389
  function cancel(target) {
219
390
  let cancelled = 0;
220
391
  if ("requestId" in target) {
@@ -224,6 +395,17 @@ export function createRequestRegistry(options) {
224
395
  cancelled++;
225
396
  return cancelled;
226
397
  }
398
+ // Queued (still waiting for an admission slot)? It has no controller
399
+ // yet, so cancel it through the cancel-before-begin tripwire: pull it
400
+ // from the FIFO and let its begin() resume into an aborted context.
401
+ // Counted as one — unlike the pure race below, this targets a known
402
+ // in-flight (queued) begin, so the Stop button feels responsive
403
+ // instead of waiting for the slot to free first.
404
+ const queued = waitersById.get(target.requestId);
405
+ if (queued) {
406
+ cancelQueuedWaiterGracefully(queued.key, queued.waiter, target.reason);
407
+ return cancelled + 1;
408
+ }
227
409
  // Stop-button race: the client beat its own
228
410
  // `begin(...)`. Record the cancel so the next matching `begin`
229
411
  // aborts immediately. The return value stays 0 — no in-flight
@@ -240,12 +422,35 @@ export function createRequestRegistry(options) {
240
422
  if (cancelEntry(entry, target.reason))
241
423
  cancelled++;
242
424
  }
425
+ // Queued waiters aren't in `entries`; drain the ones matching this
426
+ // model (and optional kind) so a broad `cancel({ modelId })` doesn't
427
+ // strand them behind a request that's being cancelled out from under
428
+ // them. Snapshot first — `removeWaiter` mutates `waitersById`.
429
+ for (const { key, waiter } of Array.from(waitersById.values())) {
430
+ if (waiter.modelId !== target.modelId)
431
+ continue;
432
+ if (target.kind && waiter.kind !== target.kind)
433
+ continue;
434
+ cancelQueuedWaiterGracefully(key, waiter, target.reason);
435
+ cancelled++;
436
+ }
243
437
  return cancelled;
244
438
  }
245
439
  function cancelAll(reason) {
246
440
  for (const entry of entries.values()) {
247
441
  cancelEntry(entry, reason);
248
442
  }
443
+ // Drain every queued waiter so a shutdown / model-unload sweep can't
444
+ // leave a `begin(...)` promise hung forever — which would in turn block
445
+ // the unload waiting on a request that never resolves. Unlike the
446
+ // targeted cancels above these *reject* rather than resolve-into-
447
+ // aborted: the model / worker they queued against is being torn down,
448
+ // so there is nothing left for them to run. Snapshot first —
449
+ // `removeWaiter` mutates `waitersById`.
450
+ for (const { key, waiter } of Array.from(waitersById.values())) {
451
+ removeWaiter(key, waiter);
452
+ waiter.reject(new RequestRejectedByPolicyError(waiter.requestId, waiter.kind, waiter.modelId, `queued request cancelled (${reason})`));
453
+ }
249
454
  // The interface returns Promise<void> so we can later make this an
250
455
  // async sweep that awaits per-handler scope unwinding (e.g. join on
251
456
  // the disposers). Today every handler unwinds on its own dispose
@@ -259,7 +464,7 @@ export function createRequestRegistry(options) {
259
464
  await disposeEntry(entry, outcome);
260
465
  }
261
466
  function policy(opts) {
262
- policies.set(opts.kind, opts);
467
+ policies.set(opts.kind, normalizePolicy(opts));
263
468
  }
264
469
  return {
265
470
  begin,
@@ -275,6 +480,43 @@ export function createRequestRegistry(options) {
275
480
  // interface (typed via the augmented return type below) so handler
276
481
  // code can't depend on it accidentally.
277
482
  __cancelBeforeBeginSize: () => cancelledBeforeBegin.size,
483
+ // Test-only: the number of live per-`(kind, modelId)` admission
484
+ // states. Lets the queue tests assert the map empties on drain /
485
+ // dispose (no `KeyState` leak). Also off the public interface.
486
+ __keyStateSize: () => keyStates.size,
487
+ };
488
+ }
489
+ /**
490
+ * Resolve a `ConcurrencyPolicy` to concrete numbers, applying defaults and
491
+ * the deprecated `oneAtATimePerModel` alias. Done once at registration so
492
+ * the `begin(...)` hot path reads plain numbers.
493
+ */
494
+ function normalizePolicy(opts) {
495
+ let maxConcurrent;
496
+ let onOverflow;
497
+ if (opts.maxConcurrentPerModel !== undefined) {
498
+ maxConcurrent = opts.maxConcurrentPerModel;
499
+ onOverflow = opts.onOverflow ?? "queue";
500
+ }
501
+ else if (opts.oneAtATimePerModel === true) {
502
+ // Legacy alias: at most one in-flight per model, reject the rest.
503
+ maxConcurrent = 1;
504
+ onOverflow = opts.onOverflow ?? "reject";
505
+ }
506
+ else {
507
+ // Nothing configured (includes `oneAtATimePerModel: false`) ⇒ no gating.
508
+ maxConcurrent = Infinity;
509
+ onOverflow = opts.onOverflow ?? "queue";
510
+ }
511
+ // A finite limit below 1 would gate every request forever; clamp to the
512
+ // smallest sensible serial limit.
513
+ if (Number.isFinite(maxConcurrent) && maxConcurrent < 1)
514
+ maxConcurrent = 1;
515
+ return {
516
+ maxConcurrent,
517
+ onOverflow,
518
+ maxQueueDepth: opts.maxQueueDepthPerModel ?? DEFAULT_MAX_QUEUE_DEPTH_PER_MODEL,
519
+ queueTimeoutMs: opts.queueTimeoutMs,
278
520
  };
279
521
  }
280
522
  /**
@@ -287,6 +529,7 @@ export function createRequestRegistry(options) {
287
529
  export const __requestRegistryTestHooks = {
288
530
  cancelBeforeBeginMaxEntries: CANCEL_BEFORE_BEGIN_MAX_ENTRIES,
289
531
  cancelBeforeBeginTtlMs: CANCEL_BEFORE_BEGIN_TTL_MS,
532
+ defaultMaxQueueDepthPerModel: DEFAULT_MAX_QUEUE_DEPTH_PER_MODEL,
290
533
  };
291
534
  function derivedTerminalState(ctx) {
292
535
  if (ctx.state === "failed")
@@ -13,7 +13,7 @@ export interface RequestLogContext {
13
13
  * the prefixed message.
14
14
  *
15
15
  * @example
16
- * await using ctx = registry.begin({ requestId, kind: "completion", modelId });
16
+ * await using ctx = await registry.begin({ requestId, kind: "completion", modelId });
17
17
  * const log = withRequestContext(getServerLogger(), ctx);
18
18
  * log.info("decoding token 7");
19
19
  * // → "[request-lifecycle completion requestId=<id> modelId=<id>] decoding token 7"
@@ -7,7 +7,7 @@
7
7
  * the prefixed message.
8
8
  *
9
9
  * @example
10
- * await using ctx = registry.begin({ requestId, kind: "completion", modelId });
10
+ * await using ctx = await registry.begin({ requestId, kind: "completion", modelId });
11
11
  * const log = withRequestContext(getServerLogger(), ctx);
12
12
  * log.info("decoding token 7");
13
13
  * // → "[request-lifecycle completion requestId=<id> modelId=<id>] decoding token 7"
@@ -37,6 +37,7 @@ export interface EmbedStats {
37
37
  tokens_per_second?: number;
38
38
  total_tokens?: number;
39
39
  backendDevice?: "cpu" | "gpu";
40
+ context_size?: number;
40
41
  }
41
42
  export interface EmbedResponse {
42
43
  stats?: EmbedStats;
@@ -53,6 +54,10 @@ export interface TranscribeStats {
53
54
  encoderMs?: number;
54
55
  decoderMs?: number;
55
56
  melSpecMs?: number;
57
+ backendDevice?: number;
58
+ backendId?: number;
59
+ gpuMemTotalMb?: number;
60
+ gpuMemFreeMb?: number;
56
61
  }
57
62
  export interface TranscribeAddonSegment {
58
63
  text: string;
@@ -9,6 +9,8 @@ import { handleUnloadModel } from "./handlers/unload-model.js";
9
9
  import { handleUnloadModelDelegated } from "./handlers/unload-model-delegated.js";
10
10
  import { handleTranscribe } from "./handlers/transcribe.js";
11
11
  import { handleTranscribeStream } from "./handlers/transcribe-stream.js";
12
+ import { handleBciTranscribe } from "./handlers/bci-transcribe.js";
13
+ import { handleBciTranscribeStream } from "./handlers/bci-transcribe-stream.js";
12
14
  import { handleEmbed } from "./handlers/embed.js";
13
15
  import { handleTranslate } from "./handlers/translate.js";
14
16
  import { handleLoggingStream } from "./handlers/logging-stream.js";
@@ -118,6 +120,8 @@ export const registry = {
118
120
  // Simple Stream handlers
119
121
  transcribe: { type: "stream", handler: handleTranscribe },
120
122
  transcribeStream: { type: "duplex", handler: handleTranscribeStream },
123
+ bciTranscribe: { type: "stream", handler: handleBciTranscribe },
124
+ bciTranscribeStream: { type: "duplex", handler: handleBciTranscribeStream },
121
125
  loggingStream: { type: "stream", handler: handleLoggingStream },
122
126
  translate: { type: "stream", handler: handleTranslate },
123
127
  textToSpeech: { type: "stream", handler: handleTextToSpeech },
@@ -0,0 +1,3 @@
1
+ import type { BciTranscribeStreamRequest, BciTranscribeStreamResponse } from "../../../schemas/index";
2
+ export declare function handleBciTranscribeStream(request: BciTranscribeStreamRequest, inputStream: AsyncIterable<Buffer>): AsyncGenerator<BciTranscribeStreamResponse>;
3
+ //# sourceMappingURL=bci-transcribe-stream.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { dispatchPluginStream } from "./plugin-dispatch.js";
2
+ export async function* handleBciTranscribeStream(request, inputStream) {
3
+ yield* dispatchPluginStream(request.modelId, "bciTranscribeStream", request, inputStream);
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { BciTranscribeRequest, BciTranscribeResponse } from "../../../schemas/index";
2
+ export declare function handleBciTranscribe(request: BciTranscribeRequest): AsyncGenerator<BciTranscribeResponse>;
3
+ //# sourceMappingURL=bci-transcribe.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { dispatchPluginStream } from "./plugin-dispatch.js";
2
+ export async function* handleBciTranscribe(request) {
3
+ yield* dispatchPluginStream(request.modelId, "bciTranscribe", request);
4
+ }
@@ -70,7 +70,7 @@ export async function handleDownloadAsset(request, progressCallback) {
70
70
  // `modelId` to register on the registry entry. Cancel by `requestId`
71
71
  // is the primary path; `cancel({ modelId })` is intentionally a
72
72
  // non-match for this kind.
73
- const ctx = __addDisposableResource(env_1, getRequestRegistry().begin({
73
+ const ctx = __addDisposableResource(env_1, await getRequestRegistry().begin({
74
74
  requestId,
75
75
  kind: "downloadAsset",
76
76
  }), true);
@@ -15,7 +15,7 @@ export async function handleGetModelInfo(request) {
15
15
  const cacheStatus = catalogEntry.shardMetadata && catalogEntry.shardMetadata.length > 0
16
16
  ? await handleShardedModel(cacheKey, catalogEntry.shardMetadata)
17
17
  : catalogEntry.companionSet
18
- ? await handleCompanionSetModel(catalogEntry.companionSet, catalogEntry.registryPath)
18
+ ? await handleCompanionSetModel(catalogEntry.companionSet, catalogEntry.registryPath, catalogEntry.addon)
19
19
  : await handleSingleFileModel(catalogEntry.registryPath, catalogEntry.expectedSize, catalogEntry.sha256Checksum);
20
20
  const { cacheFiles, isCached, actualSize, cachedAt, primaryPath } = cacheStatus;
21
21
  const loadedModelIds = getAllModelIds();
@@ -73,7 +73,7 @@ async function handleShardedModel(cacheKey, shardMetadata) {
73
73
  const paths = shardMetadata.map((s) => getShardPath(cacheKey, s.filename));
74
74
  return checkCacheStatus(fileEntries, paths);
75
75
  }
76
- async function handleCompanionSetModel(companionSet, primaryRegistryPath) {
76
+ async function handleCompanionSetModel(companionSet, primaryRegistryPath, addon) {
77
77
  const { setKey, primaryKey, files } = companionSet;
78
78
  const baseCache = getModelsCacheDir();
79
79
  const fileEntries = files.map((f) => ({
@@ -94,7 +94,7 @@ async function handleCompanionSetModel(companionSet, primaryRegistryPath) {
94
94
  if (legacyResult.isCached)
95
95
  return legacyResult;
96
96
  }
97
- else {
97
+ else if (addon === "nmt") {
98
98
  // Legacy flat-cache compatibility for Bergamot-style companion sets.
99
99
  // This is valid only for families whose runtime still works with explicit
100
100
  // per-file absolute paths; it is not a generic rule for all companion sets.
@@ -5,6 +5,8 @@ import { handleUnloadModel } from "./unload-model";
5
5
  import { handleEmbed } from "./embed";
6
6
  import { handleTranscribe } from "./transcribe";
7
7
  import { handleTranscribeStream } from "./transcribe-stream";
8
+ import { handleBciTranscribe } from "./bci-transcribe";
9
+ import { handleBciTranscribeStream } from "./bci-transcribe-stream";
8
10
  import { provideHandler } from "./provideHandler";
9
11
  import { stopProvideHandler } from "./stopProvideHandler";
10
12
  import { handleTranslate } from "./translate";
@@ -37,6 +39,8 @@ export declare const handlers: {
37
39
  embed: typeof handleEmbed;
38
40
  transcribe: typeof handleTranscribe;
39
41
  transcribeStream: typeof handleTranscribeStream;
42
+ bciTranscribe: typeof handleBciTranscribe;
43
+ bciTranscribeStream: typeof handleBciTranscribeStream;
40
44
  provide: typeof provideHandler;
41
45
  stopProvide: typeof stopProvideHandler;
42
46
  translate: typeof handleTranslate;
@@ -5,6 +5,8 @@ import { handleUnloadModel } from "./unload-model.js";
5
5
  import { handleEmbed } from "./embed.js";
6
6
  import { handleTranscribe } from "./transcribe.js";
7
7
  import { handleTranscribeStream } from "./transcribe-stream.js";
8
+ import { handleBciTranscribe } from "./bci-transcribe.js";
9
+ import { handleBciTranscribeStream } from "./bci-transcribe-stream.js";
8
10
  import { provideHandler } from "./provideHandler.js";
9
11
  import { stopProvideHandler } from "./stopProvideHandler.js";
10
12
  import { handleTranslate } from "./translate.js";
@@ -37,6 +39,8 @@ export const handlers = {
37
39
  embed: handleEmbed,
38
40
  transcribe: handleTranscribe,
39
41
  transcribeStream: handleTranscribeStream,
42
+ bciTranscribe: handleBciTranscribe,
43
+ bciTranscribeStream: handleBciTranscribeStream,
40
44
  provide: provideHandler,
41
45
  stopProvide: stopProvideHandler,
42
46
  translate: handleTranslate,
@@ -5,7 +5,7 @@ import type { DownloadHooks } from "./types";
5
5
  /**
6
6
  * Per-subscriber binding to a registry-tracked request.
7
7
  * `startOrJoinDownload` is request-aware: each caller (the
8
- * `await using ctx = registry.begin(...)` inside `handleLoadModel` /
8
+ * `await using ctx = await registry.begin(...)` inside `handleLoadModel` /
9
9
  * `handleDownloadAsset`) registers a subscriber bound to its
10
10
  * `requestId`. A `cancel({ requestId })` against the registry aborts
11
11
  * the subscriber's `ctx.signal`, which:
@@ -82,7 +82,7 @@ export async function handleLoadModel(request, progressCallback) {
82
82
  // is opened with `modelId: undefined` so a cancel-by-modelId fired
83
83
  // before the load completes is a clean no-op (rather than matching a
84
84
  // half-built entry). Cancel-by-`requestId` works from `begin(...)` on.
85
- const ctx = __addDisposableResource(env_1, getRequestRegistry().begin({
85
+ const ctx = __addDisposableResource(env_1, await getRequestRegistry().begin({
86
86
  requestId,
87
87
  kind: "loadModel",
88
88
  }), true);
@@ -1,7 +1,7 @@
1
1
  import fs, { promises as fsPromises } from "bare-fs";
2
2
  import path from "bare-path";
3
3
  import { Readable } from "bare-stream";
4
- import fetch from "bare-fetch";
4
+ import fetch, { Headers } from "bare-fetch";
5
5
  import { withTimeout } from "../../../../utils/withTimeout.js";
6
6
  import { getModelsCacheDir, getShardedModelCacheDir, generateShortHash, detectShardedModel, parsePatternBasedShardUrl, extractTensorsFromShards, calculatePercentage, isArchiveUrl, sanitizePathComponent, extractAndValidateShardedArchive, validateShardedModelCache, checkAllShardsExist, generateShardFilenames, hasValidGGUFHeader, } from "../../../utils/index.js";
7
7
  import { getSDKConfig } from "../../../bare/registry/config-registry.js";
@@ -84,11 +84,11 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
84
84
  logger.info(`📥 Starting fresh download`);
85
85
  }
86
86
  // Prepare headers for resume if needed
87
- const headers = {
87
+ const headers = new Headers({
88
88
  "User-Agent": "qvac-sdk",
89
- };
89
+ });
90
90
  if (startOffset > 0) {
91
- headers["Range"] = `bytes=${startOffset}-`;
91
+ headers.append("Range", `bytes=${startOffset}-`);
92
92
  }
93
93
  const config = getSDKConfig();
94
94
  const connectionTimeout = config.httpConnectionTimeoutMs ?? DEFAULT_HTTP_CONNECTION_TIMEOUT_MS;
@@ -134,9 +134,9 @@ async function performHttpDownload(url, modelPath, downloadKey, progressCallback
134
134
  // Retry without Range header
135
135
  response = await fetch(url, {
136
136
  method: "GET",
137
- headers: {
137
+ headers: new Headers({
138
138
  "User-Agent": "qvac-sdk",
139
- },
139
+ }),
140
140
  ...(signal && { signal }),
141
141
  });
142
142
  if (!response.ok) {
@@ -12,6 +12,7 @@ import { getSDKConfig } from "../../../bare/registry/config-registry.js";
12
12
  import { FileNotFoundError, ChecksumValidationFailedError, DownloadCancelledError, HyperdriveDownloadFailedError, ModelLoadFailedError, NoBlobFoundError, } from "../../../../utils/errors-server.js";
13
13
  import { getServerLogger } from "../../../../logging/index.js";
14
14
  import { registerSwarm, unregisterSwarm, registerCorestore, unregisterCorestore, } from "../../../bare/runtime-lifecycle.js";
15
+ import { Buffer } from "bare-buffer";
15
16
  const logger = getServerLogger();
16
17
  function getCorestoreDir(hyperdriveKey) {
17
18
  return getQvacPath("corestore", hyperdriveKey);
@@ -6,6 +6,7 @@ type CompanionSetMetadata = NonNullable<RegistryItem["companionSet"]>;
6
6
  export interface DownloadCompanionSetOptions {
7
7
  companionSet: CompanionSetMetadata;
8
8
  downloadKey: string;
9
+ allowLegacyFlatCache?: boolean | undefined;
9
10
  progressCallback?: ((progress: ModelProgressUpdate) => void) | undefined;
10
11
  signal?: AbortSignal | undefined;
11
12
  hooks?: DownloadHooks | undefined;
@@ -13,7 +13,7 @@ const logger = getServerLogger();
13
13
  * Returns the path to the primary file.
14
14
  */
15
15
  export async function downloadCompanionSetFromRegistry(options) {
16
- const { companionSet, downloadKey, progressCallback, signal, hooks, shouldClearCache, } = options;
16
+ const { companionSet, downloadKey, allowLegacyFlatCache = false, progressCallback, signal, hooks, shouldClearCache, } = options;
17
17
  const { files, setKey, primaryKey } = companionSet;
18
18
  validateCompanionSetMetadata(files, primaryKey);
19
19
  const primaryEntry = files.find((f) => f.key === primaryKey);
@@ -30,7 +30,7 @@ export async function downloadCompanionSetFromRegistry(options) {
30
30
  // Legacy flat-cache compatibility for Bergamot-style companion sets.
31
31
  // This is valid only for families whose runtime still works with explicit
32
32
  // per-file absolute paths; it is not a generic rule for all companion sets.
33
- if (!isLegacyOnnxSet(files)) {
33
+ if (allowLegacyFlatCache && !isLegacyOnnxSet(files)) {
34
34
  const flatPath = await checkLegacyFlatCache(files, primaryKey, hooks);
35
35
  if (flatPath) {
36
36
  logger.info(`✅ Using legacy flat cache for companion set: ${flatPath}`);
@@ -251,6 +251,7 @@ export async function downloadModelFromRegistry(registryPath, registrySource, pr
251
251
  return await downloadCompanionSetFromRegistry({
252
252
  companionSet: modelMetadata.companionSet,
253
253
  downloadKey,
254
+ allowLegacyFlatCache: modelMetadata.addon === "nmt",
254
255
  progressCallback: ctx.broadcastProgress,
255
256
  signal: ctx.signal,
256
257
  hooks: companionHooks,
@@ -7,7 +7,7 @@ export interface ResolveSessionOptions {
7
7
  profilingEnabled: boolean;
8
8
  /**
9
9
  * Optional cancel signal — typically `ctx.signal` from the surrounding
10
- * `await using ctx = registry.begin(...)` block. When provided,
10
+ * `await using ctx = await registry.begin(...)` block. When provided,
11
11
  * `resolveModelPath` short-circuits with `InferenceCancelledError` if the
12
12
  * signal is already aborted on entry; the same signal also propagates
13
13
  * to in-progress transfers via the request binding below so cancel