@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
@@ -0,0 +1,154 @@
1
+ import { generateConfigHash } from "../../../ops/kv-cache-utils";
2
+ import type { CacheMessage } from "../../../../utils/index";
3
+ import type { Logger } from "../../../../../logging/types";
4
+ export interface TurnHandle {
5
+ /** Resolved on-disk cache file path the addon will read from / write to. */
6
+ readonly cachePath: string;
7
+ /**
8
+ * Snapshot of the on-disk saved-message count at `beginTurn` time
9
+ * (0 if the cache was just primed). Consumed by `decideCachedHistorySlice`
10
+ * to pick the message tail for the next addon call.
11
+ */
12
+ readonly savedCount: number;
13
+ }
14
+ export interface BeginCustomTurnInput {
15
+ kind: "custom";
16
+ /** User-provided session key (`completion({ kvCache: "session-a" })`). */
17
+ customKey: string;
18
+ /** Hash of system prompt + (static) tool names. */
19
+ configHash: string;
20
+ /**
21
+ * Prime the cache by sending system prompt + (static) tools to the
22
+ * addon. Called when the cache doesn't exist in-memory OR on disk.
23
+ * Kept as an injected closure so this module has no dependency on the
24
+ * model registry / addon — the handler closes over `model` and tools.
25
+ */
26
+ primeIfMissing: (cachePath: string) => Promise<void>;
27
+ }
28
+ export interface BeginAutoTurnInput {
29
+ kind: "auto";
30
+ /** Hash of system prompt + (static) tool names. */
31
+ configHash: string;
32
+ /** Conversation history used to compute the pre-response cache key. */
33
+ history: CacheMessage[];
34
+ /** See `BeginCustomTurnInput.primeIfMissing`. */
35
+ primeIfMissing: (cachePath: string) => Promise<void>;
36
+ }
37
+ export type BeginTurnInput = BeginCustomTurnInput | BeginAutoTurnInput;
38
+ export interface StaticCommitResult {
39
+ kind: "static";
40
+ /** `history.length + 1` — recorded at the turn's current `cachePath`. */
41
+ messageCount: number;
42
+ }
43
+ export interface AutoRenameCommitResult {
44
+ kind: "autoRename";
45
+ /**
46
+ * Destination path the addon's pre-response cache file should be
47
+ * renamed to (computed from `cacheMessages + responseText`). The
48
+ * stale entry at the source path is dropped from `cachedMessageCounts`
49
+ * and the new count is recorded at this target path.
50
+ */
51
+ targetCachePath: string;
52
+ /** Number of messages the renamed cache represents (`savedHistory.length`). */
53
+ messageCount: number;
54
+ }
55
+ export type CommitResult = StaticCommitResult | AutoRenameCommitResult;
56
+ export interface KvCacheSession {
57
+ /**
58
+ * Open a new turn against the cache. Resolves the cache file path,
59
+ * primes the system-prompt cache if needed (delegated to
60
+ * `input.primeIfMissing`), marks the cache initialized, and returns a
61
+ * `TurnHandle` the handler attaches to `ctx.scope.defer(...)` for the
62
+ * rollback hook.
63
+ */
64
+ beginTurn(input: BeginTurnInput): Promise<TurnHandle>;
65
+ /**
66
+ * Commit a successful turn — records the new saved-message count,
67
+ * preserves the cache file, and (for auto-cache turns) renames the
68
+ * addon's pre-response file to the post-response path. Flips the
69
+ * turn's internal `committed` flag so the deferred `rollback` becomes
70
+ * a no-op on the happy path.
71
+ */
72
+ commitTurn(turn: TurnHandle, result: CommitResult): Promise<void>;
73
+ /**
74
+ * Roll back an in-flight turn — atomically deletes the on-disk cache
75
+ * file, clears the in-memory `initializedCaches` entry, and forgets
76
+ * the `cachedMessageCounts` entry. **All three layers, always, in
77
+ * one place.** Idempotent: a turn that has already been committed
78
+ * or rolled back is a no-op on subsequent calls. Handlers register
79
+ * this via `ctx.scope.defer(...)` so it runs regardless of how the
80
+ * handler exits (success branch removes itself via `commitTurn`).
81
+ */
82
+ rollback(turn: TurnHandle): Promise<void>;
83
+ /**
84
+ * Forget the in-memory saved-message count for the turn's path
85
+ * without unlinking the file or clearing the init flag. Used when
86
+ * `decideCachedHistorySlice` detects a stale boundary
87
+ * (`clearStaleCount: true`) — the next turn re-sends the full history
88
+ * but the cache itself is still usable.
89
+ */
90
+ dropStaleSavedCount(turn: TurnHandle): void;
91
+ }
92
+ /**
93
+ * Construct a session bound to one `(modelId, turn-owning request)`
94
+ * scope. `options.logger` is the per-instance logger the session emits
95
+ * through (typically `withRequestContext(getServerLogger(), ctx)`);
96
+ * falls back to the module-scoped logger when omitted.
97
+ */
98
+ export declare function createKvCacheSession(modelId: string, options?: {
99
+ logger?: Logger;
100
+ }): KvCacheSession;
101
+ /**
102
+ * Atomically delete every layer of KV-cache state for a
103
+ * `(kvCacheKey, modelId)` pair, or wipe everything. Single entry point
104
+ * — the only mutation point for cross-model state outside of
105
+ * turn-scoped `commitTurn`/`rollback`.
106
+ *
107
+ * Why this isn't a method on `KvCacheSession`: deletes are
108
+ * cross-model (`all: true` has no model; the keyed form has
109
+ * `modelId` optional on the wire). A session, by contrast, is
110
+ * created with a *fixed* `modelId` for the duration of a turn. Making
111
+ * delete a method would force callers to materialise an irrelevant
112
+ * session for cross-model administrative cleanups.
113
+ *
114
+ * Layers cleared, in order:
115
+ * 1. On-disk: `deleteCache(...)` removes the matching directory
116
+ * tree (or wipes and recreates the root for `all: true`).
117
+ * 2. `cachedMessageCounts`: prefix-cleanup by the removed directory
118
+ * so any per-cache count under the deleted tree is forgotten.
119
+ * 3. `initializedCaches`: scope clear by `(kvCacheKey[, modelId])`,
120
+ * matching the on-disk scope.
121
+ *
122
+ * Concurrency with in-flight turns: this delete is wire-async with
123
+ * respect to any turn currently holding a `TurnHandle` for the same
124
+ * cache key. Worst case the on-disk `.bin` is removed while a turn is
125
+ * mid-write; the turn's eventual `commitTurn(...)` then fails the
126
+ * `verifySaveAndRecord` probe (file gone) and rolls back idempotently.
127
+ * No coordination primitive is needed because every layer's mutation
128
+ * is idempotent (`unlink` no-ops if missing, `Map.delete` / `Set.delete`
129
+ * no-op on absent keys).
130
+ */
131
+ export declare function deleteKvCacheState(target: {
132
+ kvCacheKey: string;
133
+ modelId?: string;
134
+ } | {
135
+ all: true;
136
+ }): Promise<void>;
137
+ /**
138
+ * Test-only access to the module-scoped state. Production code reaches
139
+ * for cache state exclusively through the session API; the unit suite
140
+ * for `kv-cache-session.test.ts` needs to seed and inspect raw state
141
+ * to assert the rollback / commit invariants. Not part of the public
142
+ * SDK surface.
143
+ *
144
+ * @internal
145
+ */
146
+ export declare const __kvCacheSessionTestHooks: {
147
+ getSavedCount(cachePath: string): number | undefined;
148
+ setSavedCountForTest(cachePath: string, count: number): void;
149
+ hasInitializedKey(modelId: string, configHash: string, cacheKey: string): boolean;
150
+ markInitializedForTest(modelId: string, configHash: string, cacheKey: string): void;
151
+ resetForTest(): void;
152
+ };
153
+ export { generateConfigHash };
154
+ //# sourceMappingURL=kv-cache-session.d.ts.map
@@ -0,0 +1,448 @@
1
+ import { promises as fsPromises } from "bare-fs";
2
+ import path from "bare-path";
3
+ import { findMatchingCache, generateConfigHash, getCacheFilePath, getCurrentCacheInfo, renameCacheFile, deleteCache as deleteCacheUtil, } from "../../../ops/kv-cache-utils.js";
4
+ import { logCacheSaveError, logCacheStatus, } from "./cache-logger.js";
5
+ import { getServerLogger } from "../../../../../logging/index.js";
6
+ // Used by cross-model paths that have no `RequestContext` (e.g.
7
+ // `deleteKvCacheState`). Per-session call sites receive a logger from
8
+ // the caller — typically `withRequestContext(...)`.
9
+ const moduleLogger = getServerLogger();
10
+ /**
11
+ * Single owner of the three KV-cache bookkeeping layers.
12
+ *
13
+ * The llama.cpp completion handler has three independent layers it must
14
+ * keep consistent across every cancel/error branch:
15
+ *
16
+ * 1. `cachedMessageCounts: Map<path, count>` — the "n messages
17
+ * currently on disk" tracker.
18
+ * 2. `initializedCaches: Set<key>` — the "addon defers disk writes;
19
+ * we know this cache is primed" tracker.
20
+ * 3. On-disk `.bin` files written by the addon.
21
+ *
22
+ * Without a single owner, every cancel / zero-token / rename-failed /
23
+ * tool-call exit would need to touch all three; any branch that forgets
24
+ * a layer produces three-layer drift bugs.
25
+ *
26
+ * `KvCacheSession` collapses the three layers behind one object with
27
+ * three operations:
28
+ *
29
+ * - `beginTurn` — resolves the cache file path, primes the system
30
+ * prompt cache if missing (delegated to a caller-supplied closure
31
+ * so the session doesn't depend on the model addon), marks the
32
+ * cache initialized, and returns a `TurnHandle` carrying the
33
+ * resolved path + the snapshot of the on-disk saved count.
34
+ * - `commitTurn` — records the new saved count (for custom-key
35
+ * turns) or renames the addon's pre-response file to the
36
+ * post-response path and records the count there (for auto-cache
37
+ * turns). Flips the turn's internal `committed` flag so the
38
+ * deferred `rollback` becomes a no-op on the happy path.
39
+ * - `rollback` — atomically deletes the on-disk file, clears the
40
+ * in-memory init entry, and forgets the saved count. **All three
41
+ * layers, always, in one place.** Handlers call it once via
42
+ * `ctx.scope.defer(() => session.rollback(turn))`; `commitTurn`
43
+ * short-circuits it on success.
44
+ *
45
+ * The module-level `deleteKvCacheState(...)` function (below) provides
46
+ * an administrative cross-model delete API for the
47
+ * `handleDeleteCache` RPC handler.
48
+ *
49
+ * The module-scoped `cachedMessageCounts` and `initializedCaches` maps
50
+ * are *private* to this file — no other module reaches into them.
51
+ * Callers that need cache-status info do so through the session API.
52
+ */
53
+ // ----- module-scoped state. The session is the single mutation point
54
+ // for the in-memory KV-cache bookkeeping. -----
55
+ /**
56
+ * Number of chat messages the kv-cache file on disk is known to cover,
57
+ * keyed by cache path. Written by `commitTurn`, read by `getSavedCount`,
58
+ * deleted by `rollback` / `delete` / `dropStaleSavedCount`. The same
59
+ * INVARIANT that existed in `kv-cache-state.ts` still holds: an entry is
60
+ * present only when the corresponding `.bin` file is considered
61
+ * trustworthy. Cancelled or zero-token turns must remove the entry so
62
+ * the next-turn slice doesn't read a stale boundary.
63
+ */
64
+ const cachedMessageCounts = new Map();
65
+ /**
66
+ * In-memory registry of caches initialized this session. The addon
67
+ * defers disk writes, so the absence of a `.bin` file on disk isn't
68
+ * proof that the cache hasn't been primed in this worker process. Keyed
69
+ * by `${modelId}:${configHash}:${cacheKey}`, so on-disk caches from
70
+ * older worker runs still hit the lazy-load path in `beginTurn`.
71
+ */
72
+ const initializedCaches = new Set();
73
+ function initRegistryKey(modelId, configHash, cacheKey) {
74
+ return `${modelId}:${configHash}:${cacheKey}`;
75
+ }
76
+ // ----- factory -----
77
+ /**
78
+ * Construct a session bound to one `(modelId, turn-owning request)`
79
+ * scope. `options.logger` is the per-instance logger the session emits
80
+ * through (typically `withRequestContext(getServerLogger(), ctx)`);
81
+ * falls back to the module-scoped logger when omitted.
82
+ */
83
+ export function createKvCacheSession(modelId, options) {
84
+ const logger = options?.logger ?? moduleLogger;
85
+ // Per-session map: each `TurnHandle` carries an opaque entry here. A
86
+ // WeakMap so handles drop their state once the handler scope releases
87
+ // the reference; the module-scoped maps above survive.
88
+ const turnState = new WeakMap();
89
+ function makeHandle(cachePath, registryKey) {
90
+ const handle = {
91
+ cachePath,
92
+ savedCount: cachedMessageCounts.get(cachePath) ?? 0,
93
+ };
94
+ turnState.set(handle, {
95
+ cachePath,
96
+ registryKey,
97
+ committed: false,
98
+ rolledBack: false,
99
+ });
100
+ return handle;
101
+ }
102
+ async function beginCustom(input) {
103
+ const cachePath = await getCacheFilePath(modelId, input.configHash, input.customKey);
104
+ const registryKey = initRegistryKey(modelId, input.configHash, input.customKey);
105
+ // In-memory registry check first — the addon defers disk writes, so
106
+ // a freshly-primed cache may not yet exist on disk. If the
107
+ // in-memory flag isn't set, fall back to a filesystem probe so
108
+ // caches surviving across worker restarts still hit the reuse path.
109
+ let exists = initializedCaches.has(registryKey);
110
+ if (!exists) {
111
+ try {
112
+ await fsPromises.access(cachePath);
113
+ exists = true;
114
+ initializedCaches.add(registryKey);
115
+ }
116
+ catch {
117
+ exists = false;
118
+ }
119
+ }
120
+ logCacheStatus(input.customKey, exists);
121
+ if (!exists) {
122
+ await input.primeIfMissing(cachePath);
123
+ await verifyPrimedFile(cachePath, logger);
124
+ initializedCaches.add(registryKey);
125
+ }
126
+ return makeHandle(cachePath, registryKey);
127
+ }
128
+ async function beginAuto(input) {
129
+ // The pre-response cache key is derived from
130
+ // `history.slice(0, -1)` — `findMatchingCache` does that
131
+ // internally. The post-response key (used after a successful turn)
132
+ // is computed by the caller and passed to `commitTurn` as
133
+ // `targetCachePath`.
134
+ const existingCache = await findMatchingCache(modelId, input.configHash, input.history);
135
+ const preResponseCacheInfo = await getCurrentCacheInfo(modelId, input.configHash, input.history);
136
+ const cachePath = existingCache !== null
137
+ ? existingCache.cachePath
138
+ : preResponseCacheInfo.cachePath;
139
+ const cacheKeyForRegistry = existingCache !== null
140
+ ? existingCache.cacheKey
141
+ : preResponseCacheInfo.cacheKey;
142
+ const registryKey = initRegistryKey(modelId, input.configHash, cacheKeyForRegistry);
143
+ const cacheExists = existingCache !== null;
144
+ logCacheStatus("auto", cacheExists);
145
+ if (!cacheExists) {
146
+ await input.primeIfMissing(cachePath);
147
+ await verifyPrimedFile(cachePath, logger);
148
+ initializedCaches.add(registryKey);
149
+ }
150
+ return makeHandle(cachePath, registryKey);
151
+ }
152
+ async function beginTurn(input) {
153
+ if (input.kind === "custom")
154
+ return beginCustom(input);
155
+ return beginAuto(input);
156
+ }
157
+ async function commitTurn(turn, result) {
158
+ const state = turnState.get(turn);
159
+ if (!state) {
160
+ // Handle from a different session or already torn down. Treat as
161
+ // no-op — caller shouldn't be reaching into another session's
162
+ // state, but failing loudly here punishes the rollback-after-end
163
+ // path more than it helps.
164
+ return;
165
+ }
166
+ if (state.committed || state.rolledBack)
167
+ return;
168
+ if (result.kind === "static") {
169
+ // Custom-key path: the addon wrote the new cache state inline
170
+ // at the same path. Verify the file persisted (the addon
171
+ // currently swallows save errors — see TODO in
172
+ // `verifySaveAndRecord`) and record the new boundary.
173
+ const ok = await verifySaveAndRecord(state.cachePath, result.messageCount);
174
+ if (!ok) {
175
+ // The expected save didn't land — treat the turn as a rollback
176
+ // so the next turn re-primes cleanly.
177
+ await runRollback(state);
178
+ return;
179
+ }
180
+ state.committed = true;
181
+ return;
182
+ }
183
+ // Auto-rename path: the pre-response file is now stale (its key
184
+ // refers to history minus the last user turn). Rename it to the
185
+ // post-response key and record the new count there.
186
+ if (!(await renameCacheFile(state.cachePath, result.targetCachePath))) {
187
+ logger.warn(`[kv-cache] Auto cache rename failed; rolling back. from=${state.cachePath} to=${result.targetCachePath}`);
188
+ await runRollback(state);
189
+ return;
190
+ }
191
+ // The source path's entry is gone (the file moved). Drop it and
192
+ // record the new count at the rename target.
193
+ cachedMessageCounts.delete(state.cachePath);
194
+ const ok = await verifySaveAndRecord(result.targetCachePath, result.messageCount);
195
+ if (!ok) {
196
+ // Rename succeeded but the file isn't where we expected. Roll
197
+ // back via the target path instead of the (now-empty) source.
198
+ state.cachePath = result.targetCachePath;
199
+ await runRollback(state);
200
+ return;
201
+ }
202
+ // Successful auto-rename. The handle's `cachePath` field still
203
+ // points at the (now-gone) source path — that's fine, the handle
204
+ // is committed and won't roll back. Future turns compute fresh
205
+ // paths.
206
+ state.committed = true;
207
+ }
208
+ async function rollback(turn) {
209
+ const state = turnState.get(turn);
210
+ if (!state)
211
+ return;
212
+ if (state.committed || state.rolledBack)
213
+ return;
214
+ await runRollback(state);
215
+ }
216
+ async function runRollback(state) {
217
+ // Order matters only weakly: unlink first so a partial disk-state
218
+ // can't be re-loaded by a sibling turn between the file delete and
219
+ // the in-memory clear. In practice handlers serialise per model;
220
+ // the order is belt-and-suspenders.
221
+ try {
222
+ await fsPromises.unlink(state.cachePath);
223
+ }
224
+ catch (unlinkError) {
225
+ logger.warn(`[kv-cache] Failed to remove cache file during rollback; next turn may load stale KV state. path=${state.cachePath} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
226
+ }
227
+ initializedCaches.delete(state.registryKey);
228
+ cachedMessageCounts.delete(state.cachePath);
229
+ state.rolledBack = true;
230
+ }
231
+ function dropStaleSavedCount(turn) {
232
+ const state = turnState.get(turn);
233
+ if (!state)
234
+ return;
235
+ cachedMessageCounts.delete(state.cachePath);
236
+ }
237
+ return {
238
+ beginTurn,
239
+ commitTurn,
240
+ rollback,
241
+ dropStaleSavedCount,
242
+ };
243
+ }
244
+ // ----- module-level administrative API -----
245
+ /**
246
+ * Atomically delete every layer of KV-cache state for a
247
+ * `(kvCacheKey, modelId)` pair, or wipe everything. Single entry point
248
+ * — the only mutation point for cross-model state outside of
249
+ * turn-scoped `commitTurn`/`rollback`.
250
+ *
251
+ * Why this isn't a method on `KvCacheSession`: deletes are
252
+ * cross-model (`all: true` has no model; the keyed form has
253
+ * `modelId` optional on the wire). A session, by contrast, is
254
+ * created with a *fixed* `modelId` for the duration of a turn. Making
255
+ * delete a method would force callers to materialise an irrelevant
256
+ * session for cross-model administrative cleanups.
257
+ *
258
+ * Layers cleared, in order:
259
+ * 1. On-disk: `deleteCache(...)` removes the matching directory
260
+ * tree (or wipes and recreates the root for `all: true`).
261
+ * 2. `cachedMessageCounts`: prefix-cleanup by the removed directory
262
+ * so any per-cache count under the deleted tree is forgotten.
263
+ * 3. `initializedCaches`: scope clear by `(kvCacheKey[, modelId])`,
264
+ * matching the on-disk scope.
265
+ *
266
+ * Concurrency with in-flight turns: this delete is wire-async with
267
+ * respect to any turn currently holding a `TurnHandle` for the same
268
+ * cache key. Worst case the on-disk `.bin` is removed while a turn is
269
+ * mid-write; the turn's eventual `commitTurn(...)` then fails the
270
+ * `verifySaveAndRecord` probe (file gone) and rolls back idempotently.
271
+ * No coordination primitive is needed because every layer's mutation
272
+ * is idempotent (`unlink` no-ops if missing, `Map.delete` / `Set.delete`
273
+ * no-op on absent keys).
274
+ */
275
+ export async function deleteKvCacheState(target) {
276
+ if ("all" in target) {
277
+ const removed = await deleteCacheUtil({ all: true });
278
+ cachedMessageCounts.clear();
279
+ initializedCaches.clear();
280
+ // `removed` is the kv-cache root dir; surfaces it for ops
281
+ // visibility but isn't part of the contract.
282
+ moduleLogger.debug(`[kv-cache] Cleared all caches under ${removed}`);
283
+ return;
284
+ }
285
+ const removedPath = await deleteCacheUtil({
286
+ kvCacheKey: target.kvCacheKey,
287
+ ...(target.modelId !== undefined && { modelId: target.modelId }),
288
+ });
289
+ // Prefix-cleanup the in-memory counts. The on-disk directory tree
290
+ // is `{kvCacheRoot}/{kvCacheKey}[/{modelId}]/`, so every entry in
291
+ // `cachedMessageCounts` whose key is the removed directory itself
292
+ // or sits beneath it must go.
293
+ clearCachedMessageCountsByPrefix(removedPath, path.sep);
294
+ // The in-memory init-set keys are
295
+ // `${modelId}:${configHash}:${kvCacheKey}` — clear by the user-
296
+ // facing kvCacheKey (and optionally narrow by modelId).
297
+ clearInitializedCachesByScope({
298
+ cacheKey: target.kvCacheKey,
299
+ ...(target.modelId !== undefined && { modelId: target.modelId }),
300
+ });
301
+ }
302
+ // ----- private helpers -----
303
+ /**
304
+ * Verify that the addon actually persisted a usable cache file after a
305
+ * prime. Mirrors the `verifySaveAndRecord` access-probe used at commit
306
+ * time, applied at prime time so the session doesn't mark a cache
307
+ * `initializedCaches.add(...)` against a path that's missing or empty
308
+ * on disk.
309
+ *
310
+ * Failure modes this catches:
311
+ *
312
+ * - The addon's `model.run({ saveSessionPath })` was interrupted
313
+ * before the save call ran (e.g. signal abort during prefill); the
314
+ * prime closure resolves cleanly because addon save errors are not
315
+ * propagated, but no file is on disk.
316
+ * - The addon's `llama_state_save_file` was called but produced an
317
+ * empty file (out-of-space / fs error swallowed by the addon).
318
+ *
319
+ * Failure modes this does **NOT** catch:
320
+ *
321
+ * - A partial-but-nonzero file written by the addon (e.g. header +
322
+ * truncated KV state). Catching this requires either an
323
+ * addon-side change (have `CacheManager::writeCacheFile` check the
324
+ * return value of `llama_state_save_file` and throw on failure) or
325
+ * a structural hash check we can't currently compute from the
326
+ * SDK. Filed as a follow-up — see `cache-api.md` in the addon
327
+ * repo / tracking ticket.
328
+ *
329
+ * On failure we best-effort `unlink` an empty leftover file (so the
330
+ * next existence probe doesn't trust it) and throw — the handler in
331
+ * `completion-stream.ts` lets the error propagate up and no
332
+ * `initializedCaches` entry is recorded.
333
+ */
334
+ async function verifyPrimedFile(cachePath, logger) {
335
+ let stats;
336
+ try {
337
+ stats = await fsPromises.stat(cachePath);
338
+ }
339
+ catch (statError) {
340
+ // ENOENT is the common case here — addon prime returned without
341
+ // calling save (most often: signal abort during prefill).
342
+ throw new Error(`[kv-cache] prime closure resolved but no cache file was written. path=${cachePath} cause=${statError instanceof Error ? statError.message : String(statError)}`);
343
+ }
344
+ if (stats.size === 0) {
345
+ // Best-effort cleanup so a future probe doesn't trust the empty
346
+ // file. Unlink failure is non-fatal — we still throw on the
347
+ // primary "prime didn't persist" condition.
348
+ try {
349
+ await fsPromises.unlink(cachePath);
350
+ }
351
+ catch (unlinkError) {
352
+ logger.warn(`[kv-cache] Failed to remove empty primed cache file. path=${cachePath} error=${unlinkError instanceof Error ? unlinkError.message : String(unlinkError)}`);
353
+ }
354
+ throw new Error(`[kv-cache] prime closure resolved but cache file is empty. path=${cachePath}`);
355
+ }
356
+ }
357
+ /**
358
+ * Verify the addon actually persisted the cache file before recording
359
+ * its message count. The addon currently swallows write errors
360
+ * silently, so a missing file means the next turn must resend the full
361
+ * history rather than slicing against a stale `savedCount`.
362
+ *
363
+ * TODO: once the addon surfaces save failures (e.g. throws
364
+ * `UnableToSaveSessionFile` when `llama_state_save_file` returns
365
+ * false), drop the `access()` probe and wrap the `model.run()` call in
366
+ * a real try/catch that forwards the error.
367
+ */
368
+ async function verifySaveAndRecord(cachePath, messageCount) {
369
+ try {
370
+ await fsPromises.access(cachePath);
371
+ cachedMessageCounts.set(cachePath, messageCount);
372
+ return true;
373
+ }
374
+ catch (err) {
375
+ cachedMessageCounts.delete(cachePath);
376
+ logCacheSaveError(cachePath, err);
377
+ return false;
378
+ }
379
+ }
380
+ function clearCachedMessageCountsByPrefix(prefix, sep) {
381
+ if (!prefix) {
382
+ cachedMessageCounts.clear();
383
+ return;
384
+ }
385
+ for (const key of cachedMessageCounts.keys()) {
386
+ if (key === prefix) {
387
+ cachedMessageCounts.delete(key);
388
+ continue;
389
+ }
390
+ if (!key.startsWith(prefix + sep))
391
+ continue;
392
+ cachedMessageCounts.delete(key);
393
+ }
394
+ }
395
+ function clearInitializedCachesByScope(scope) {
396
+ if (scope.cacheKey === undefined && scope.modelId === undefined) {
397
+ initializedCaches.clear();
398
+ return;
399
+ }
400
+ for (const key of initializedCaches) {
401
+ const firstSep = key.indexOf(":");
402
+ const secondSep = key.indexOf(":", firstSep + 1);
403
+ if (firstSep === -1 || secondSep === -1)
404
+ continue;
405
+ const entryModelId = key.slice(0, firstSep);
406
+ const entryCacheKey = key.slice(secondSep + 1);
407
+ if (scope.cacheKey !== undefined && entryCacheKey !== scope.cacheKey) {
408
+ continue;
409
+ }
410
+ if (scope.modelId !== undefined && entryModelId !== scope.modelId) {
411
+ continue;
412
+ }
413
+ initializedCaches.delete(key);
414
+ }
415
+ }
416
+ // ----- test-only escape hatches -----
417
+ /**
418
+ * Test-only access to the module-scoped state. Production code reaches
419
+ * for cache state exclusively through the session API; the unit suite
420
+ * for `kv-cache-session.test.ts` needs to seed and inspect raw state
421
+ * to assert the rollback / commit invariants. Not part of the public
422
+ * SDK surface.
423
+ *
424
+ * @internal
425
+ */
426
+ export const __kvCacheSessionTestHooks = {
427
+ getSavedCount(cachePath) {
428
+ return cachedMessageCounts.get(cachePath);
429
+ },
430
+ setSavedCountForTest(cachePath, count) {
431
+ cachedMessageCounts.set(cachePath, count);
432
+ },
433
+ hasInitializedKey(modelId, configHash, cacheKey) {
434
+ return initializedCaches.has(initRegistryKey(modelId, configHash, cacheKey));
435
+ },
436
+ markInitializedForTest(modelId, configHash, cacheKey) {
437
+ initializedCaches.add(initRegistryKey(modelId, configHash, cacheKey));
438
+ },
439
+ resetForTest() {
440
+ cachedMessageCounts.clear();
441
+ initializedCaches.clear();
442
+ },
443
+ };
444
+ // Re-export `generateConfigHash` from the path utilities so callers of
445
+ // the session can compute the hash without separately importing
446
+ // `kv-cache-utils`. The function itself stays in `kv-cache-utils.ts`
447
+ // (pure, no state) — only the re-export lives here.
448
+ export { generateConfigHash };