@qvac/sdk 0.10.2 → 0.11.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 (200) hide show
  1. package/NOTICE +92 -54
  2. package/dist/client/api/cancel.d.ts +24 -32
  3. package/dist/client/api/cancel.js +58 -31
  4. package/dist/client/api/client-request-id.d.ts +17 -0
  5. package/dist/client/api/client-request-id.js +27 -0
  6. package/dist/client/api/completion-stream.d.ts +1 -1
  7. package/dist/client/api/completion-stream.js +34 -3
  8. package/dist/client/api/diffusion.d.ts +46 -0
  9. package/dist/client/api/diffusion.js +50 -1
  10. package/dist/client/api/download-asset.d.ts +12 -2
  11. package/dist/client/api/download-asset.js +21 -3
  12. package/dist/client/api/embed.d.ts +8 -4
  13. package/dist/client/api/embed.js +14 -1
  14. package/dist/client/api/get-model-info.d.ts +1 -1
  15. package/dist/client/api/index.d.ts +1 -0
  16. package/dist/client/api/index.js +1 -0
  17. package/dist/client/api/load-model.d.ts +12 -4
  18. package/dist/client/api/load-model.js +25 -1
  19. package/dist/client/api/rag.d.ts +9 -3
  20. package/dist/client/api/rag.js +23 -3
  21. package/dist/client/api/transcribe.d.ts +20 -8
  22. package/dist/client/api/transcribe.js +58 -4
  23. package/dist/client/api/unload-model.d.ts +5 -2
  24. package/dist/client/api/unload-model.js +9 -4
  25. package/dist/client/api/upscale.d.ts +45 -0
  26. package/dist/client/api/upscale.js +99 -0
  27. package/dist/client/rpc/expo-rpc-client.js +2 -1
  28. package/dist/client/rpc/node-rpc-client.js +18 -7
  29. package/dist/client/rpc/rpc-client.d.ts +2 -2
  30. package/dist/client/rpc/rpc-client.js +10 -4
  31. package/dist/client/rpc/rpc-error.d.ts +9 -0
  32. package/dist/client/rpc/rpc-error.js +92 -0
  33. package/dist/examples/cancel-by-request-id.d.ts +38 -0
  34. package/dist/examples/cancel-by-request-id.js +103 -0
  35. package/dist/examples/diffusion-esrgan-upscale.d.ts +2 -0
  36. package/dist/examples/diffusion-esrgan-upscale.js +78 -0
  37. package/dist/examples/download-with-cancel.js +10 -10
  38. package/dist/examples/embed-multi-gpu.d.ts +2 -0
  39. package/dist/examples/embed-multi-gpu.js +46 -0
  40. package/dist/examples/llamacpp-multi-gpu.d.ts +2 -0
  41. package/dist/examples/llamacpp-multi-gpu.js +57 -0
  42. package/dist/examples/rag/rag-hyperdb/cancellation.js +20 -17
  43. package/dist/examples/tools/llamacpp-tools-gemma4.d.ts +2 -0
  44. package/dist/examples/tools/llamacpp-tools-gemma4.js +76 -0
  45. package/dist/examples/tools/llamacpp-tools-qwen35.d.ts +2 -0
  46. package/dist/examples/tools/llamacpp-tools-qwen35.js +70 -0
  47. package/dist/examples/transcription/whispercpp-microphone-conversation.d.ts +2 -0
  48. package/dist/examples/transcription/whispercpp-microphone-conversation.js +102 -0
  49. package/dist/examples/tts/supertonic.js +1 -1
  50. package/dist/expo/plugins/withMobileBundle.d.ts +20 -4
  51. package/dist/expo/plugins/withMobileBundle.js +44 -6
  52. package/dist/index.d.ts +5 -2
  53. package/dist/index.js +21 -1
  54. package/dist/models/registry/models.d.ts +186 -318
  55. package/dist/models/registry/models.js +1522 -1667
  56. package/dist/pear/pre.d.ts +3 -0
  57. package/dist/pear/pre.js +3 -0
  58. package/dist/schemas/cancel.d.ts +167 -28
  59. package/dist/schemas/cancel.js +119 -19
  60. package/dist/schemas/common.d.ts +427 -208
  61. package/dist/schemas/common.js +3 -1
  62. package/dist/schemas/completion-event.d.ts +14 -0
  63. package/dist/schemas/completion-event.js +11 -1
  64. package/dist/schemas/completion-stream.d.ts +15 -1
  65. package/dist/schemas/completion-stream.js +25 -7
  66. package/dist/schemas/download-asset.d.ts +15 -9
  67. package/dist/schemas/download-asset.js +17 -6
  68. package/dist/schemas/embed.d.ts +1 -0
  69. package/dist/schemas/embed.js +5 -0
  70. package/dist/schemas/error.d.ts +31 -0
  71. package/dist/schemas/error.js +29 -1
  72. package/dist/schemas/finetune.d.ts +2 -0
  73. package/dist/schemas/finetune.js +5 -0
  74. package/dist/schemas/get-model-info.d.ts +6 -6
  75. package/dist/schemas/llamacpp-config.d.ts +54 -8
  76. package/dist/schemas/llamacpp-config.js +12 -0
  77. package/dist/schemas/load-model.d.ts +1911 -1260
  78. package/dist/schemas/load-model.js +15 -0
  79. package/dist/schemas/model-src-utils.d.ts +14 -14
  80. package/dist/schemas/model-types.d.ts +10 -10
  81. package/dist/schemas/ocr.d.ts +3 -3
  82. package/dist/schemas/plugin.d.ts +40 -1
  83. package/dist/schemas/plugin.js +10 -1
  84. package/dist/schemas/rag.d.ts +9 -0
  85. package/dist/schemas/rag.js +17 -9
  86. package/dist/schemas/registry.d.ts +30 -30
  87. package/dist/schemas/reload-config.d.ts +14 -14
  88. package/dist/schemas/sdcpp-config.d.ts +131 -17
  89. package/dist/schemas/sdcpp-config.js +163 -1
  90. package/dist/schemas/sdk-config.d.ts +649 -271
  91. package/dist/schemas/sdk-errors-client.d.ts +1 -0
  92. package/dist/schemas/sdk-errors-client.js +5 -0
  93. package/dist/schemas/sdk-errors-server.d.ts +5 -0
  94. package/dist/schemas/sdk-errors-server.js +25 -0
  95. package/dist/schemas/text-to-speech.d.ts +78 -78
  96. package/dist/schemas/transcription-config.d.ts +24 -24
  97. package/dist/schemas/transcription.d.ts +56 -2
  98. package/dist/schemas/transcription.js +27 -0
  99. package/dist/schemas/translate.d.ts +2 -0
  100. package/dist/schemas/translate.js +13 -2
  101. package/dist/schemas/translation-config.d.ts +40 -40
  102. package/dist/schemas/unload-model.d.ts +2 -1
  103. package/dist/schemas/unload-model.js +4 -1
  104. package/dist/scripts/run-unit-tests.js +15 -2
  105. package/dist/server/bare/ops/cancel.d.ts +27 -1
  106. package/dist/server/bare/ops/cancel.js +36 -10
  107. package/dist/server/bare/ops/embed.d.ts +3 -3
  108. package/dist/server/bare/ops/embed.js +144 -28
  109. package/dist/server/bare/ops/kv-cache-utils.d.ts +0 -7
  110. package/dist/server/bare/ops/kv-cache-utils.js +8 -47
  111. package/dist/server/bare/ops/transcribe.d.ts +10 -5
  112. package/dist/server/bare/ops/transcribe.js +196 -41
  113. package/dist/server/bare/ops/translate.d.ts +1 -1
  114. package/dist/server/bare/ops/translate.js +197 -88
  115. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.d.ts +12 -3
  116. package/dist/server/bare/plugins/llamacpp-completion/ops/completion-stream.js +170 -172
  117. package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +125 -21
  118. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.d.ts +154 -0
  119. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-session.js +448 -0
  120. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.d.ts +21 -73
  121. package/dist/server/bare/plugins/llamacpp-completion/ops/kv-cache-state.js +18 -101
  122. package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +30 -3
  123. package/dist/server/bare/plugins/llamacpp-completion/plugin.js +157 -85
  124. package/dist/server/bare/plugins/llamacpp-completion/transform.d.ts +10 -0
  125. package/dist/server/bare/plugins/llamacpp-completion/transform.js +34 -0
  126. package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +7 -0
  127. package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +10 -1
  128. package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +17 -15
  129. package/dist/server/bare/plugins/nmtcpp-translation/plugin.js +5 -1
  130. package/dist/server/bare/plugins/onnx-ocr/plugin.d.ts +3 -3
  131. package/dist/server/bare/plugins/onnx-ocr/plugin.js +3 -0
  132. package/dist/server/bare/plugins/onnx-tts/plugin.d.ts +39 -39
  133. package/dist/server/bare/plugins/onnx-tts/plugin.js +6 -0
  134. package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +29 -21
  135. package/dist/server/bare/plugins/parakeet-transcription/plugin.js +4 -1
  136. package/dist/server/bare/plugins/sdcpp-generation/ops/diffusion.js +8 -0
  137. package/dist/server/bare/plugins/sdcpp-generation/ops/upscale.d.ts +3 -0
  138. package/dist/server/bare/plugins/sdcpp-generation/ops/upscale.js +40 -0
  139. package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +91 -16
  140. package/dist/server/bare/plugins/sdcpp-generation/plugin.js +97 -7
  141. package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +22 -3
  142. package/dist/server/bare/plugins/whispercpp-transcription/plugin.js +50 -14
  143. package/dist/server/bare/rag-hyperdb/index.d.ts +1 -1
  144. package/dist/server/bare/rag-hyperdb/index.js +1 -1
  145. package/dist/server/bare/rag-hyperdb/rag-operation-manager.d.ts +26 -4
  146. package/dist/server/bare/rag-hyperdb/rag-operation-manager.js +65 -28
  147. package/dist/server/bare/runtime/disposable-scope.d.ts +36 -0
  148. package/dist/server/bare/runtime/disposable-scope.js +69 -0
  149. package/dist/server/bare/runtime/index.d.ts +8 -0
  150. package/dist/server/bare/runtime/index.js +3 -0
  151. package/dist/server/bare/runtime/request-context.d.ts +49 -0
  152. package/dist/server/bare/runtime/request-context.js +1 -0
  153. package/dist/server/bare/runtime/request-id.d.ts +12 -0
  154. package/dist/server/bare/runtime/request-id.js +14 -0
  155. package/dist/server/bare/runtime/request-registry-singleton.d.ts +4 -0
  156. package/dist/server/bare/runtime/request-registry-singleton.js +27 -0
  157. package/dist/server/bare/runtime/request-registry.d.ts +136 -0
  158. package/dist/server/bare/runtime/request-registry.js +297 -0
  159. package/dist/server/bare/runtime/with-request-context.d.ts +22 -0
  160. package/dist/server/bare/runtime/with-request-context.js +53 -0
  161. package/dist/server/bare/types/addon-responses.d.ts +11 -1
  162. package/dist/server/rpc/handler-registry.js +26 -7
  163. package/dist/server/rpc/handlers/cancel-delegated.js +28 -31
  164. package/dist/server/rpc/handlers/cancelHandler.d.ts +30 -1
  165. package/dist/server/rpc/handlers/cancelHandler.js +54 -22
  166. package/dist/server/rpc/handlers/delete-cache.d.ts +9 -0
  167. package/dist/server/rpc/handlers/delete-cache.js +13 -15
  168. package/dist/server/rpc/handlers/download-asset.js +133 -40
  169. package/dist/server/rpc/handlers/index.d.ts +2 -0
  170. package/dist/server/rpc/handlers/index.js +2 -0
  171. package/dist/server/rpc/handlers/load-model/download-manager.d.ts +60 -2
  172. package/dist/server/rpc/handlers/load-model/download-manager.js +141 -8
  173. package/dist/server/rpc/handlers/load-model/handler.js +213 -98
  174. package/dist/server/rpc/handlers/load-model/http.js +3 -3
  175. package/dist/server/rpc/handlers/load-model/hyperdrive.js +1 -1
  176. package/dist/server/rpc/handlers/load-model/registry.js +1 -1
  177. package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +20 -1
  178. package/dist/server/rpc/handlers/load-model/resolve-session.js +6 -5
  179. package/dist/server/rpc/handlers/load-model/resolve.d.ts +3 -2
  180. package/dist/server/rpc/handlers/load-model/resolve.js +9 -6
  181. package/dist/server/rpc/handlers/load-model/types.d.ts +24 -0
  182. package/dist/server/rpc/handlers/rag.js +133 -15
  183. package/dist/server/rpc/handlers/upscale-stream.d.ts +3 -0
  184. package/dist/server/rpc/handlers/upscale-stream.js +4 -0
  185. package/dist/server/utils/completion-normalizer.js +8 -0
  186. package/dist/server/utils/tools/dialect.js +4 -0
  187. package/dist/server/utils/tools/parser.js +16 -1
  188. package/dist/server/utils/tools/parsers/gemma4native.d.ts +4 -0
  189. package/dist/server/utils/tools/parsers/gemma4native.js +60 -0
  190. package/dist/server/utils/tools/parsers/qwen35.d.ts +4 -0
  191. package/dist/server/utils/tools/parsers/qwen35.js +120 -0
  192. package/dist/utils/aggregate-events.d.ts +9 -0
  193. package/dist/utils/aggregate-events.js +18 -3
  194. package/dist/utils/decorate-promise.d.ts +34 -0
  195. package/dist/utils/decorate-promise.js +35 -0
  196. package/dist/utils/errors-client.d.ts +3 -0
  197. package/dist/utils/errors-client.js +5 -0
  198. package/dist/utils/errors-server.d.ts +85 -0
  199. package/dist/utils/errors-server.js +97 -0
  200. package/package.json +20 -15
@@ -1,8 +1,9 @@
1
1
  import { TOOLS_MODE } from "../../../../../schemas/tools.js";
2
- import { logCacheDisabled, logCacheInit, logCacheSave, logCacheSaveError, logCacheStatus, logMessagesToAddon, } from "./cache-logger.js";
3
- import { clearCacheRegistry, customCacheExists, extractSystemPrompt, findMatchingCache, generateConfigHash, getCacheFilePath, getCurrentCacheInfo, markCacheInitialized, renameCacheFile, } from "../../../ops/kv-cache-utils.js";
2
+ import { logCacheDisabled, logCacheInit, logCacheSave, logMessagesToAddon, } from "./cache-logger.js";
3
+ import { extractSystemPrompt, getCurrentCacheInfo, } from "../../../ops/kv-cache-utils.js";
4
4
  import { getModel, getModelConfig, } from "../../../registry/model-registry.js";
5
- import { cachedMessageCounts, clearCachedMessageCounts as clearCachedMessageCountsFromState, decideCachedHistorySlice, noteCancelRequested as noteCancelRequestedFromState, shouldRecordSavedCount, snapshotCancelCount, } from "./kv-cache-state.js";
5
+ import { decideCachedHistorySlice } from "./kv-cache-state.js";
6
+ import { createKvCacheSession, generateConfigHash, } from "./kv-cache-session.js";
6
7
  import { appendToolsToHistory, detectToolDialect, prependToolsToHistory, } from "../../../../utils/tool-integration.js";
7
8
  import { parseToolCalls } from "../../../../utils/tools/index.js";
8
9
  import { getResponseFormatJsonSchema } from "../../../../utils/response-format.js";
@@ -11,37 +12,23 @@ import { getServerLogger } from "../../../../../logging/index.js";
11
12
  import { AttachmentNotFoundError } from "../../../../../utils/errors-server.js";
12
13
  import { nowMs } from "../../../../../profiling/index.js";
13
14
  import { buildStreamResult, hasDefinedValues, } from "../../../../../profiling/model-execution.js";
14
- import fs, { promises as fsPromises } from "bare-fs";
15
- import path from "bare-path";
15
+ import fs from "bare-fs";
16
16
  const logger = getServerLogger();
17
- // Re-export so existing callers keep their import surface intact. The pure
18
- // state module has no `bare-*` imports, so we inject the platform path
19
- // separator here — without this, prefix-based clears would miss entries
20
- // under directory keys on Windows.
21
- export function clearCachedMessageCounts(prefix) {
22
- clearCachedMessageCountsFromState(prefix, path.sep);
23
- }
24
- export const noteCancelRequested = noteCancelRequestedFromState;
25
- // Verify the addon actually persisted the cache file before recording its
26
- // message count. The addon currently swallows write errors silently, so a
27
- // missing file means the next turn must resend the full history rather than
28
- // slicing against a stale `savedCount`.
29
- //
30
- // TODO: once the addon surfaces save failures (e.g. throws
31
- // `UnableToSaveSessionFile` when `llama_state_save_file` returns false),
32
- // drop the `access()` probe and wrap the `model.run()` call in a real
33
- // try/catch that forwards the error to `logCacheSaveError`.
34
- async function recordCacheSaveCount(cachePath, messageCount) {
35
- try {
36
- await fsPromises.access(cachePath);
37
- cachedMessageCounts.set(cachePath, messageCount);
38
- return true;
39
- }
40
- catch (err) {
41
- cachedMessageCounts.delete(cachePath);
42
- logCacheSaveError(cachePath, err);
43
- return false;
44
- }
17
+ /**
18
+ * Decide whether a completed turn earned the right to record its kv-cache
19
+ * boundary. A `savedCount` is only safe to write when the turn ran to
20
+ * completion AND produced at least one token — anything else (cancelled
21
+ * mid-decode, zero-token reply, early EOS) leaves the on-disk cache file
22
+ * in an unknown state relative to `history.length + 1`, and a stale entry
23
+ * would slice the next turn's history down to an empty payload.
24
+ *
25
+ * Replaces the pre-0.11.0 `shouldRecordSavedCount(wasCancelled, ...)` with
26
+ * a signal-driven check that reads directly from the request's
27
+ * `AbortSignal`. The local helper keeps the call sites in
28
+ * `completion-stream.ts` honest without importing the registry every time.
29
+ */
30
+ function shouldCommitTurn(signal, producedTokens) {
31
+ return !signal.aborted && producedTokens;
45
32
  }
46
33
  function transformMessage(message) {
47
34
  const transformed = [];
@@ -100,9 +87,7 @@ async function initSystemPromptCache(model, cachePathToUse, systemPromptToUse, c
100
87
  const primeResponse = await runModel(model, primeMessages, {
101
88
  cacheKey: cachePathToUse,
102
89
  saveCacheToDisk: true,
103
- });
104
- primeResponse.once("output", () => {
105
- void primeResponse.cancel();
90
+ prefill: true,
106
91
  });
107
92
  await primeResponse.await();
108
93
  }
@@ -117,7 +102,8 @@ async function initSystemPromptCache(model, cachePathToUse, systemPromptToUse, c
117
102
  * consumer pushing both an assistant transcript and a follow-up user
118
103
  * message between completions) all reaches the model.
119
104
  * - Cache hit with a stale/missing `savedCount`: fall back to the full
120
- * non-system history.
105
+ * non-system history. The session is told (`dropStaleSavedCount`) so
106
+ * the bad boundary doesn't propagate into the next turn.
121
107
  *
122
108
  * Dynamic mode (`tools` argument set):
123
109
  * - The addon anchors the tool block after the last user message and
@@ -133,7 +119,7 @@ async function initSystemPromptCache(model, cachePathToUse, systemPromptToUse, c
133
119
  * the new prompt, then re-anchor the tool block.
134
120
  * * otherwise: send just the last message + tool block.
135
121
  */
136
- function prepareMessagesForCache(cachePathToUse, cacheExists, history, tools) {
122
+ function prepareMessagesForCache(session, turn, cacheExists, history, tools) {
137
123
  const addTools = tools?.length ? transformMessages(tools) : [];
138
124
  const dynamic = addTools.length > 0;
139
125
  if (!(cacheExists && history.length > 0)) {
@@ -141,16 +127,18 @@ function prepareMessagesForCache(cachePathToUse, cacheExists, history, tools) {
141
127
  return [...transformMessages(historyWithoutSystem), ...addTools];
142
128
  }
143
129
  if (!dynamic) {
144
- // Static path — slice from the recorded `savedCount` so callers can
130
+ // Static path — slice from the turn's `savedCount` so callers can
145
131
  // stage multiple messages between completions. `decideCachedHistorySlice`
146
132
  // also guards against the QVAC-17780 stale-count regression: if the
147
133
  // saved boundary would slice the history down to an empty payload
148
134
  // (e.g. after a cancelled mid-decode), it falls back to the full
149
135
  // non-system history and signals the caller to drop the bad entry.
150
- const savedCount = cachedMessageCounts.get(cachePathToUse) ?? 0;
151
- const { messages, clearStaleCount } = decideCachedHistorySlice(savedCount, cacheExists, history);
136
+ // The session owns the entry; `dropStaleSavedCount` clears it
137
+ // without touching the on-disk file (the file is still trustworthy
138
+ // — only the boundary count is wrong).
139
+ const { messages, clearStaleCount } = decideCachedHistorySlice(turn.savedCount, cacheExists, history);
152
140
  if (clearStaleCount) {
153
- cachedMessageCounts.delete(cachePathToUse);
141
+ session.dropStaleSavedCount(turn);
154
142
  }
155
143
  return transformMessages(messages);
156
144
  }
@@ -231,8 +219,10 @@ async function* processModelResponse(model, messagesToSend, tools, generationPar
231
219
  producedTokens,
232
220
  };
233
221
  }
234
- export async function* completion(params) {
222
+ export async function* completion(params, opts) {
235
223
  const { history, modelId, kvCache, tools, generationParams, responseFormat } = params;
224
+ const { signal, scope } = opts;
225
+ const requestLogger = opts.logger ?? logger;
236
226
  const modelConfig = getModelConfig(modelId);
237
227
  const toolsEnabled = modelConfig.tools === true;
238
228
  const toolsMode = modelConfig.toolsMode;
@@ -259,136 +249,45 @@ export async function* completion(params) {
259
249
  }
260
250
  }
261
251
  const model = getModel(modelId);
262
- if (kvCache) {
263
- const systemPromptFromHistory = extractSystemPrompt(history);
264
- // Dynamic mode lets each turn carry its own tool set, so the cache
265
- // hash must not depend on the tool list — otherwise a tool change
266
- // would force a fresh cache file and defeat the whole optimisation.
267
- const configHash = generateConfigHash(systemPromptFromHistory, dynamicTools ? undefined : tools);
268
- const systemPromptToUse = systemPromptFromHistory ||
269
- modelConfig.system_prompt ||
270
- "You are a helpful assistant.";
271
- let cachePathToUse;
272
- if (typeof kvCache === "string") {
273
- cachePathToUse = await getCacheFilePath(modelId, configHash, kvCache);
274
- let cacheExists = await customCacheExists(modelId, configHash, kvCache);
275
- logCacheStatus(kvCache, cacheExists);
276
- if (!cacheExists) {
277
- await initSystemPromptCache(model, cachePathToUse, systemPromptToUse, kvCache,
278
- // Static-mode tools are baked into the system-prompt cache so
279
- // they're shared across the session. Dynamic-mode tools belong
280
- // to a per-turn anchor and must not enter the system cache.
281
- staticTools ? tools : undefined);
282
- markCacheInitialized(modelId, configHash, kvCache);
283
- cacheExists = true;
284
- }
285
- const messagesToSend = prepareMessagesForCache(cachePathToUse, cacheExists, history, dynamicTools ? tools : undefined);
286
- logMessagesToAddon(messagesToSend, "PROMPT_SEND");
287
- const cancelCountBefore = snapshotCancelCount(modelId);
288
- const result = yield* processModelResponse(model, messagesToSend, tools, mergedGenerationParams, { cacheKey: cachePathToUse, saveCacheToDisk: true }, dialect);
289
- const wasCancelled = snapshotCancelCount(modelId) > cancelCountBefore;
290
- if (shouldRecordSavedCount(wasCancelled, result.producedTokens)) {
291
- // Turn ran to completion and produced content — record the new
292
- // boundary so the next turn can slice its history.
293
- await recordCacheSaveCount(cachePathToUse, history.length + 1);
294
- }
295
- else {
296
- // The addon writes the cache file unconditionally on
297
- // `saveCacheToDisk` turns, including cancellations and zero-token
298
- // exits, so what's left on disk holds partial decode state that
299
- // does not correspond to a clean turn boundary. Mirror the
300
- // auto-key handling: drop the file, clear the in-memory init
301
- // flag (otherwise `customCacheExists` would still report true),
302
- // and forget the saved count. Next turn re-primes the system
303
- // prompt cleanly — a one-turn perf hit, but no risk of the
304
- // addon loading the stale KV state.
305
- try {
306
- await fsPromises.unlink(cachePathToUse);
307
- }
308
- catch (unlinkError) {
309
- logger.warn(`[kv-cache] Failed to remove cache file after cancelled or empty custom-key turn; next turn may load stale KV state. path=${cachePathToUse} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
310
- }
311
- clearCacheRegistry({ cacheKey: kvCache, modelId });
312
- cachedMessageCounts.delete(cachePathToUse);
313
- }
314
- return result;
315
- }
316
- else {
317
- // Auto-generate cache key based on conversation history
318
- const cacheMessages = history.map((msg) => ({
319
- role: msg.role,
320
- content: msg.content,
321
- attachments: msg.attachments ?? undefined,
322
- }));
323
- const existingCache = await findMatchingCache(modelId, configHash, cacheMessages);
324
- const preResponseCacheInfo = await getCurrentCacheInfo(modelId, configHash, cacheMessages);
325
- cachePathToUse =
326
- existingCache !== null
327
- ? existingCache.cachePath
328
- : preResponseCacheInfo.cachePath;
329
- let cacheExists = existingCache !== null;
330
- logCacheStatus("auto", cacheExists);
331
- if (!cacheExists) {
332
- await initSystemPromptCache(model, cachePathToUse, systemPromptToUse, "auto", staticTools ? tools : undefined);
333
- markCacheInitialized(modelId, configHash, preResponseCacheInfo.cacheKey);
334
- cacheExists = true;
335
- }
336
- const messagesToSend = prepareMessagesForCache(cachePathToUse, cacheExists, history, dynamicTools ? tools : undefined);
337
- logMessagesToAddon(messagesToSend, "PROMPT_SEND");
338
- const cancelCountBefore = snapshotCancelCount(modelId);
339
- const result = yield* processModelResponse(model, messagesToSend, tools, mergedGenerationParams, { cacheKey: cachePathToUse, saveCacheToDisk: true }, dialect);
340
- const wasCancelled = snapshotCancelCount(modelId) > cancelCountBefore;
341
- // TODO: support auto-cache for tool-call turns by keying off the
342
- // structured assistant/tool messages callers push into history,
343
- // not result.responseText (which is raw tool-call markup here).
344
- // Until then, remove any cache file the addon wrote so it doesn't
345
- // leak on disk (the next turn would compute a different key and
346
- // never reach it).
347
- if (result.toolCalls.length > 0) {
348
- logger.warn(`[kv-cache] Auto cache tool-call turn; removing orphaned cache to avoid disk leak. path=${cachePathToUse}`);
349
- try {
350
- await fsPromises.unlink(cachePathToUse);
351
- }
352
- catch (unlinkError) {
353
- logger.warn(`[kv-cache] Failed to remove orphaned tool-turn cache file; disk leak likely. path=${cachePathToUse} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
354
- }
355
- cachedMessageCounts.delete(cachePathToUse);
356
- return result;
357
- }
358
- // A cancelled or zero-token turn cannot be promoted to a post-response
359
- // cache: the post-response key is derived from `result.responseText`,
360
- // which is empty/partial in those cases, and the on-disk cache the
361
- // addon wrote is not aligned with the current-history hash. Treat it
362
- // like the tool-call branch — drop the cache file and clear the count.
363
- if (!shouldRecordSavedCount(wasCancelled, result.producedTokens)) {
364
- try {
365
- await fsPromises.unlink(cachePathToUse);
366
- }
367
- catch (unlinkError) {
368
- logger.warn(`[kv-cache] Failed to remove cache file after cancelled or empty turn; disk leak possible. path=${cachePathToUse} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
369
- }
370
- cachedMessageCounts.delete(cachePathToUse);
371
- return result;
372
- }
373
- const savedHistory = buildAutoCacheSaveHistory(cacheMessages, result.responseText);
374
- const postResponseCacheInfo = await getCurrentCacheInfo(modelId, configHash, savedHistory);
375
- if (!(await renameCacheFile(cachePathToUse, postResponseCacheInfo.cachePath))) {
376
- logger.warn(`[kv-cache] Auto cache rename failed; removing stale cache to avoid disk leak. from=${cachePathToUse} to=${postResponseCacheInfo.cachePath}`);
377
- try {
378
- await fsPromises.unlink(cachePathToUse);
379
- }
380
- catch (unlinkError) {
381
- logger.warn(`[kv-cache] Failed to remove stale cache file; disk leak likely. path=${cachePathToUse} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
382
- }
383
- cachedMessageCounts.delete(cachePathToUse);
384
- return result;
385
- }
386
- cachedMessageCounts.delete(cachePathToUse);
387
- await recordCacheSaveCount(postResponseCacheInfo.cachePath, savedHistory.length);
388
- return result;
252
+ // Hard-cancel wiring: when the registry aborts the request's signal,
253
+ // forward to the addon so the C++ work stops as soon as it can. The
254
+ // SDK still treats `signal.aborted` as the truth for cancel detection
255
+ // (post-completion bookkeeping below) — this listener only shortens
256
+ // the latency between "user clicked stop" and "addon stops decoding".
257
+ //
258
+ // Fire-and-forget by construction (event listeners can't `await`), but
259
+ // `addon.cancel()` returns a Promise — if it ever rejects the bare
260
+ // `void` would leak it as an unhandledRejection. Attach `.catch(...)`
261
+ // so a rejection is logged and the process stays clean; the iterator
262
+ // below still sees EOF/empty tokens via the addon's normal cancel path
263
+ // so callers aren't affected.
264
+ const onAbort = () => {
265
+ const addon = model.addon;
266
+ if (addon?.cancel) {
267
+ addon.cancel.call(addon).catch((err) => {
268
+ requestLogger.warn(`[cancel] addon.cancel() rejected during abort for modelId=${modelId}: ${err instanceof Error ? err.message : String(err)}`);
269
+ });
389
270
  }
390
- }
391
- else {
271
+ };
272
+ signal.addEventListener("abort", onAbort, { once: true });
273
+ // `addEventListener("abort", ..., { once: true })` does *not* fire if
274
+ // the signal is already aborted at register time — but the registry
275
+ // synchronously aborts a fresh controller when `parentSignal` was
276
+ // already aborted at `begin(...)`. Without this fall-through, the
277
+ // addon would keep decoding until the post-loop check notices.
278
+ // Re-using `onAbort` here keeps the listener body as the single
279
+ // source of truth for "what cancel does."
280
+ if (signal.aborted)
281
+ onAbort();
282
+ // Detach the abort listener on every exit path (happy, throw, generator
283
+ // `return()` from upstream). `{ once: true }` already removes the
284
+ // listener if the signal fires, so the `removeEventListener` here is
285
+ // the cleanup hook for the signal-never-fired path.
286
+ scope.defer(() => {
287
+ signal.removeEventListener("abort", onAbort);
288
+ });
289
+ if (!kvCache) {
290
+ // KV-cache disabled — straight passthrough, no session involvement.
392
291
  let historyWithTools = history;
393
292
  if (staticTools && tools) {
394
293
  historyWithTools = prependToolsToHistory(history, tools);
@@ -401,4 +300,103 @@ export async function* completion(params) {
401
300
  logMessagesToAddon(transformedHistory, "NO_CACHE");
402
301
  return yield* processModelResponse(model, transformedHistory, tools, mergedGenerationParams, undefined, dialect);
403
302
  }
303
+ // ---- KV-cache path. The session owns all three bookkeeping layers
304
+ // (on-disk `.bin`, `initializedCaches`, `cachedMessageCounts`). The
305
+ // handler asks for a turn, registers rollback on the scope, and on
306
+ // the happy path calls `commitTurn` which short-circuits the deferred
307
+ // rollback. Cancellations / zero-token replies / rename failures all
308
+ // unwind through the same `scope.defer` hook. ----
309
+ const session = createKvCacheSession(modelId, { logger: requestLogger });
310
+ const systemPromptFromHistory = extractSystemPrompt(history);
311
+ // Dynamic mode lets each turn carry its own tool set, so the cache
312
+ // hash must not depend on the tool list — otherwise a tool change
313
+ // would force a fresh cache file and defeat the whole optimisation.
314
+ const configHash = generateConfigHash(systemPromptFromHistory, dynamicTools ? undefined : tools);
315
+ const systemPromptToUse = systemPromptFromHistory ||
316
+ modelConfig.system_prompt ||
317
+ "You are a helpful assistant.";
318
+ const primeIfMissing = async (cachePath) => {
319
+ await initSystemPromptCache(model, cachePath, systemPromptToUse, typeof kvCache === "string" ? kvCache : "auto",
320
+ // Static-mode tools are baked into the system-prompt cache so
321
+ // they're shared across the session. Dynamic-mode tools belong
322
+ // to a per-turn anchor and must not enter the system cache.
323
+ staticTools ? tools : undefined);
324
+ };
325
+ let turn;
326
+ if (typeof kvCache === "string") {
327
+ turn = await session.beginTurn({
328
+ kind: "custom",
329
+ customKey: kvCache,
330
+ configHash,
331
+ primeIfMissing,
332
+ });
333
+ }
334
+ else {
335
+ const cacheMessages = history.map((msg) => ({
336
+ role: msg.role,
337
+ content: msg.content,
338
+ attachments: msg.attachments ?? undefined,
339
+ }));
340
+ turn = await session.beginTurn({
341
+ kind: "auto",
342
+ configHash,
343
+ history: cacheMessages,
344
+ primeIfMissing,
345
+ });
346
+ }
347
+ // Single cleanup hook for every non-success exit path. `commitTurn`
348
+ // flips the turn's internal `committed` flag so this becomes a no-op
349
+ // on the happy path. Scope unwinding is LIFO — registered after the
350
+ // `removeEventListener` defer above so rollback runs before the
351
+ // listener detach.
352
+ scope.defer(() => session.rollback(turn));
353
+ // `cacheExists` is implied by `beginTurn` — the session either found
354
+ // an existing cache or just primed one. Pass `true` to the message
355
+ // selector so the slicing branches engage.
356
+ const messagesToSend = prepareMessagesForCache(session, turn,
357
+ /* cacheExists */ true, history, dynamicTools ? tools : undefined);
358
+ logMessagesToAddon(messagesToSend, "PROMPT_SEND");
359
+ const result = yield* processModelResponse(model, messagesToSend, tools, mergedGenerationParams, { cacheKey: turn.cachePath, saveCacheToDisk: true }, dialect);
360
+ if (typeof kvCache === "string") {
361
+ // Custom-key path: the addon wrote the new cache state inline at
362
+ // the same path. Either commit (records the boundary, suppresses
363
+ // rollback) or fall through to the deferred rollback.
364
+ if (shouldCommitTurn(signal, result.producedTokens)) {
365
+ await session.commitTurn(turn, {
366
+ kind: "static",
367
+ messageCount: history.length + 1,
368
+ });
369
+ }
370
+ return result;
371
+ }
372
+ // Auto-cache path.
373
+ //
374
+ // Tool-call turns: the auto-cache key is derived from
375
+ // `result.responseText`, which here is raw tool-call markup rather
376
+ // than a clean assistant message. There's no safe post-response key
377
+ // to rename to, so we let the deferred rollback drop the file. Once
378
+ // the SDK supports auto-cache for structured assistant/tool turns,
379
+ // this becomes a normal commit path.
380
+ if (result.toolCalls.length > 0) {
381
+ logger.warn(`[kv-cache] Auto cache tool-call turn; rolling back to avoid disk leak. path=${turn.cachePath}`);
382
+ return result;
383
+ }
384
+ if (!shouldCommitTurn(signal, result.producedTokens)) {
385
+ // Cancelled or zero-token turn — the addon wrote the file but its
386
+ // contents don't correspond to a clean turn boundary. Let the
387
+ // deferred rollback unlink it.
388
+ return result;
389
+ }
390
+ const savedHistory = buildAutoCacheSaveHistory(history.map((msg) => ({
391
+ role: msg.role,
392
+ content: msg.content,
393
+ attachments: msg.attachments ?? undefined,
394
+ })), result.responseText);
395
+ const postResponseCacheInfo = await getCurrentCacheInfo(modelId, configHash, savedHistory);
396
+ await session.commitTurn(turn, {
397
+ kind: "autoRename",
398
+ targetCachePath: postResponseCacheInfo.cachePath,
399
+ messageCount: savedHistory.length,
400
+ });
401
+ return result;
404
402
  }
@@ -1,6 +1,61 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import fs from "bare-fs";
2
54
  import { getModel, } from "../../../registry/model-registry.js";
3
55
  import { CompletionFailedError, } from "../../../../../utils/errors-server.js";
56
+ import { getRequestRegistry, withRequestContext, } from "../../../runtime/index.js";
57
+ import { generateServerRequestId } from "../../../runtime/request-id.js";
58
+ import { getServerLogger } from "../../../../../logging/index.js";
4
59
  const PAUSE_CHECKPOINT_PREFIX = "pause_checkpoint_step_";
5
60
  const finetuneRuntimeState = new Set();
6
61
  function getRunningFinetuneState(modelId) {
@@ -46,30 +101,67 @@ function validateExplicitFinetuneOperation(request) {
46
101
  }
47
102
  }
48
103
  export async function startFinetune(request, onProgress) {
49
- const model = getModel(request.modelId);
50
- validateExplicitFinetuneOperation(request);
51
- registerRunningFinetune(request.modelId);
104
+ const env_1 = { stack: [], error: void 0, hasError: false };
52
105
  try {
106
+ const model = getModel(request.modelId);
107
+ validateExplicitFinetuneOperation(request);
108
+ // Open a request-scoped lifecycle so finetune slots into the same
109
+ // registry-driven cancel surface as the streaming inference kinds.
110
+ // `cancel({ requestId })` and broad `cancel({ modelId, kind: "finetune" })`
111
+ // both route through this context's signal — the `onAbort` listener
112
+ // forwards to `model.cancel()` to match the plugin's
113
+ // `cancel: { scope: "model", hard: true }` declaration. The legacy
114
+ // `cancelFinetune(modelId)` wrapper below now goes through the
115
+ // registry instead of touching the addon directly.
116
+ const ctx = __addDisposableResource(env_1, getRequestRegistry().begin({
117
+ requestId: request.requestId ?? generateServerRequestId(),
118
+ kind: "finetune",
119
+ modelId: request.modelId,
120
+ }), true);
121
+ const requestLogger = withRequestContext(getServerLogger(), ctx);
122
+ registerRunningFinetune(request.modelId);
123
+ // Two-level try/finally collapses into a pair of `scope.defer`
124
+ // registrations. LIFO order — the listener-detach defer is
125
+ // registered after `clearFinetuneRuntimeState`, so on scope unwind
126
+ // the listener is removed first, then the runtime-state flag is
127
+ // cleared. This mirrors the legacy `finally` nesting where the inner
128
+ // `removeListener` ran before the outer `clearFinetuneRuntimeState`.
129
+ ctx.scope.defer(() => {
130
+ clearFinetuneRuntimeState(request.modelId);
131
+ });
132
+ const onAbort = () => {
133
+ model.cancel().catch((err) => {
134
+ requestLogger.warn(`[cancel] model.cancel() rejected during abort for modelId=${request.modelId}: ${err instanceof Error ? err.message : String(err)}`);
135
+ });
136
+ };
137
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
138
+ if (ctx.signal.aborted)
139
+ onAbort();
140
+ ctx.scope.defer(() => {
141
+ ctx.signal.removeEventListener("abort", onAbort);
142
+ });
53
143
  const handle = await model.finetune(request.options);
54
144
  if (onProgress) {
55
145
  handle.on("stats", onProgress);
56
- }
57
- try {
58
- const result = await handle.await();
59
- return {
60
- type: "finetune",
61
- status: result.status,
62
- stats: result.stats,
63
- };
64
- }
65
- finally {
66
- if (onProgress) {
146
+ ctx.scope.defer(() => {
67
147
  handle.removeListener("stats", onProgress);
68
- }
148
+ });
69
149
  }
150
+ const result = await handle.await();
151
+ return {
152
+ type: "finetune",
153
+ status: result.status,
154
+ stats: result.stats,
155
+ };
156
+ }
157
+ catch (e_1) {
158
+ env_1.error = e_1;
159
+ env_1.hasError = true;
70
160
  }
71
161
  finally {
72
- clearFinetuneRuntimeState(request.modelId);
162
+ const result_1 = __disposeResources(env_1);
163
+ if (result_1)
164
+ await result_1;
73
165
  }
74
166
  }
75
167
  export async function pauseFinetune(modelId) {
@@ -80,13 +172,25 @@ export async function pauseFinetune(modelId) {
80
172
  status: "PAUSED",
81
173
  };
82
174
  }
83
- export async function cancelFinetune(modelId) {
84
- const model = getModel(modelId);
85
- await model.cancel();
86
- return {
175
+ // Thin compat wrapper over the request registry. The in-flight
176
+ // finetune is tracked by a `RequestContext` (kind `"finetune"`), and
177
+ // the registry owns the broadcast to its `AbortSignal`. `startFinetune`
178
+ // installs the addon-level `model.cancel()` listener tied to that
179
+ // signal, so callers see the same observable effect as a direct
180
+ // `model.cancel()` call. The addon-call wiring is centralised there —
181
+ // never invoke `model.cancel()` here.
182
+ export function cancelFinetune(modelId) {
183
+ // `registry.cancel(...)` is synchronous — it triggers the matching
184
+ // requests' abort signals and returns the cancelled count. Scope
185
+ // unwinding (and the `model.cancel()` forward installed by
186
+ // `startFinetune`) happens on the handler's own dispose path. The
187
+ // outer `Promise.resolve(...)` keeps the legacy `cancelFinetune`
188
+ // return shape (`Promise<FinetuneResult>`) intact for callers.
189
+ getRequestRegistry().cancel({ modelId, kind: "finetune" });
190
+ return Promise.resolve({
87
191
  type: "finetune",
88
192
  status: "CANCELLED",
89
- };
193
+ });
90
194
  }
91
195
  export function getFinetuneState(params) {
92
196
  const runtimeState = getRunningFinetuneState(params.modelId);