@bendyline/gezel-inference 1.0.6 → 1.0.8

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 (152) hide show
  1. package/dist/cache/controller.d.ts +2 -0
  2. package/dist/cache/controller.js +38 -5
  3. package/dist/catalog/chat-models/index.json +1 -1
  4. package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
  5. package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
  6. package/dist/catalog/chat-models/raw-index.json +1 -1
  7. package/dist/catalog/image-loras/index.json +1 -0
  8. package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
  9. package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
  10. package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
  11. package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
  12. package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
  13. package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
  14. package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
  15. package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
  16. package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
  17. package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
  18. package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
  19. package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
  20. package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
  21. package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
  22. package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
  23. package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
  24. package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
  25. package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
  26. package/dist/catalog/image-loras/raw-index.json +1 -0
  27. package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
  28. package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
  29. package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
  30. package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
  31. package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
  32. package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
  33. package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
  34. package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
  35. package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
  36. package/dist/catalog/image-models/index.json +1 -1
  37. package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
  38. package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
  39. package/dist/catalog/image-models/raw-index.json +1 -1
  40. package/dist/catalog/knowledge-catalogs/index.json +1 -1
  41. package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
  42. package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
  43. package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
  44. package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
  45. package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
  46. package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
  47. package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
  48. package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
  49. package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
  50. package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
  51. package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
  52. package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
  53. package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
  54. package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
  55. package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
  56. package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
  57. package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
  58. package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
  59. package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
  60. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
  61. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
  62. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
  63. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
  64. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
  65. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
  66. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
  67. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
  68. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
  69. package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
  70. package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
  71. package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
  72. package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
  73. package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
  74. package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
  75. package/dist/catalog/provenance.json +2 -1
  76. package/dist/engine-components.js +2 -2
  77. package/dist/engines/electron-native-reuse.d.ts +7 -1
  78. package/dist/engines/electron-native-reuse.js +12 -4
  79. package/dist/engines/native-file-manifest.json +546 -406
  80. package/dist/engines/native-manifest.d.ts +4 -4
  81. package/dist/engines/native-manifest.js +36 -36
  82. package/dist/engines/resolver.js +1 -0
  83. package/dist/http/auth-context.d.ts +12 -0
  84. package/dist/http/routes/image-ensure-routes.d.ts +4 -0
  85. package/dist/http/routes/image-ensure-routes.js +102 -0
  86. package/dist/http/routes/v1-image-styles.d.ts +5 -0
  87. package/dist/http/routes/v1-image-styles.js +91 -0
  88. package/dist/http/routes/v1-images.d.ts +3 -1
  89. package/dist/http/routes/v1-images.js +48 -99
  90. package/dist/inference-service.js +2 -1
  91. package/dist/knowledge/manager.d.ts +3 -27
  92. package/dist/knowledge/manager.js +8 -364
  93. package/dist/knowledge/unified-search.d.ts +61 -0
  94. package/dist/knowledge/unified-search.js +373 -0
  95. package/dist/knowledge/worker-host.js +2 -0
  96. package/dist/machine-engine/inspect-runtime.js +12 -3
  97. package/dist/memory/embed-core.js +3 -0
  98. package/dist/memory/embeddings.js +2 -0
  99. package/dist/models/image-fit.js +2 -2
  100. package/dist/models/weight-facts.d.ts +5 -0
  101. package/dist/models/weight-facts.js +5 -2
  102. package/dist/providers/ds4/build-provider.js +1 -1
  103. package/dist/providers/ds4/residency.js +3 -3
  104. package/dist/providers/image/img2img-support.d.ts +36 -1
  105. package/dist/providers/image/img2img-support.js +30 -1
  106. package/dist/providers/image/local-engine.d.ts +1 -0
  107. package/dist/providers/image/local-engine.js +8 -2
  108. package/dist/providers/image/local-factory.d.ts +10 -1
  109. package/dist/providers/image/local-factory.js +33 -6
  110. package/dist/providers/image/mock.d.ts +8 -1
  111. package/dist/providers/image/mock.js +46 -1
  112. package/dist/providers/image/pull-registry.d.ts +25 -3
  113. package/dist/providers/image/pull-registry.js +96 -34
  114. package/dist/providers/image/sd-cpp.d.ts +63 -4
  115. package/dist/providers/image/sd-cpp.js +368 -38
  116. package/dist/providers/image/types.d.ts +76 -4
  117. package/dist/providers/image/types.js +8 -0
  118. package/dist/providers/llama-cpp/build-provider.js +3 -2
  119. package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
  120. package/dist/providers/llama-cpp/cache-adapter.js +18 -4
  121. package/dist/providers/llama-cpp/devices.js +2 -2
  122. package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
  123. package/dist/providers/llama-cpp/engine-flags.js +5 -2
  124. package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
  125. package/dist/providers/mlx/async-file-handoff.js +32 -4
  126. package/dist/providers/mlx/provider.js +41 -20
  127. package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
  128. package/dist/providers/mlx/python/spec_decode.py +44 -5
  129. package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
  130. package/dist/providers/mlx/runtime-diagnostics.js +18 -1
  131. package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
  132. package/dist/providers/mlx/tool-call-protocol.js +4 -3
  133. package/dist/providers/native/capacity-broker.d.ts +1 -1
  134. package/dist/providers/native/capacity-broker.js +8 -8
  135. package/dist/providers/native/capacity-budget.js +5 -4
  136. package/dist/providers/native/device-capacity-ledger.d.ts +5 -1
  137. package/dist/providers/native/device-capacity-ledger.js +60 -14
  138. package/dist/providers/native/device-capacity.js +4 -3
  139. package/dist/providers/native/local-engine-runtime.js +4 -4
  140. package/dist/providers/native/process-identity.d.ts +40 -0
  141. package/dist/providers/native/process-identity.js +156 -0
  142. package/dist/relevance/relevance-core.js +2 -0
  143. package/dist/relevance/relevance-model.js +2 -0
  144. package/dist/relevance/settings.js +2 -2
  145. package/dist/runtime-lock.d.ts +19 -5
  146. package/dist/runtime-lock.js +116 -26
  147. package/dist/system/host-memory.d.ts +46 -0
  148. package/dist/system/host-memory.js +89 -0
  149. package/dist/system/memory.js +5 -4
  150. package/dist/transformers-cache.js +2 -0
  151. package/dist/utils/download-common.d.ts +1 -1
  152. package/package.json +8 -8
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Explicit knowledge search across mounted catalogs: each catalog searched in
3
+ * its own embedding space, the lexical and vector arms fused by reciprocal
4
+ * rank. Platform-neutral — the daemon's KnowledgeManager and the on-device
5
+ * Capacitor host both run it over their own KnowledgeCatalogHost.
6
+ */
7
+ import { formatKnowledgeUri, scoreResult, } from '@bendyline/gezel';
8
+ import { spatialDocumentIdentity } from './spatial-query.js';
9
+ /** Explicit-search shard budget (S) across every active catalog (§3.4). */
10
+ const ROUTE_BUDGET_EXPLICIT = 6;
11
+ /**
12
+ * Shards added beyond that budget for pages the query names by title (the
13
+ * catalog host's title-assisted routing): the centroids alone left the ABBA
14
+ * article unscanned for "Who were the members of ABBA?". Two, not one per
15
+ * catalog, so twenty mounted catalogs cannot add twenty scans.
16
+ */
17
+ const TITLE_ROUTE_EXPLICIT = 2;
18
+ const FINAL_K = 24;
19
+ /**
20
+ * Explicit search fuses three arms per catalog by reciprocal rank (k = 60):
21
+ * the vector chunks (best chunk per document, by rerank cosine), the
22
+ * doc-title FTS hits, and the chunk-body FTS hits. Rank fusion instead of
23
+ * score comparison because the arms' scores are not commensurable — e5
24
+ * cosines sit in a narrow 0.7–0.9 band for everything, so a cosine-first
25
+ * ordering buried an exact title match under any vaguely related passage
26
+ * ("Who painted the Mona Lisa?" listed three other Mona Lisa copies above
27
+ * the article whose title the question names). Two lexical arms agreeing
28
+ * on a document now outrank one vector arm; a document every arm names
29
+ * comes first.
30
+ */
31
+ const RRF_K = 60;
32
+ const ARM_WEIGHTS = { vector: 1, docFts: 1, chunkFts: 0.5 };
33
+ /**
34
+ * Media rows fuse per row, not per document: a document's own text chunk
35
+ * (cosine ≈ 0.9) would otherwise always stand in for its image (≈ 0.7). Each
36
+ * modality is ranked on its own scale, past its own measured floor.
37
+ */
38
+ const MEDIA_ARM_WEIGHTS = { vector: 0.5, chunkFts: 0.25 };
39
+ /** Media rows asked of each catalog, per modality, on an explicit search. */
40
+ const MEDIA_K = 2;
41
+ /** Media results one search returns at most, and per document. */
42
+ const MEDIA_RESULT_CAP = 4;
43
+ const MEDIA_PER_DOCUMENT = 1;
44
+ /**
45
+ * Fused documents per catalog when several are in scope: keeps one catalog
46
+ * from monopolizing the merged list. A search scoped to one catalog has
47
+ * nothing to share the list with, and the cap cut the Knowledge browser's
48
+ * search for "blob" in an Azure-docs catalog — 348 matching titles — to six.
49
+ */
50
+ const PER_CATALOG_CAP = 6;
51
+ /** Lexical candidates fetched per arm before fusion, at the least. */
52
+ const DOC_FTS_MIN = 6;
53
+ const CHUNK_FTS_MIN = 8;
54
+ /** Rank-anchored relevance for a fused knowledge result: the best hit is a strong 1.0, decaying RRF-style. */
55
+ function fusedRankRelevance(rank) {
56
+ return 11 / (11 + rank);
57
+ }
58
+ /**
59
+ * The first prose paragraph of a document body, for a hit that no chunk
60
+ * carries (a title-only match): the model gets something to read, not just
61
+ * a summary line. Skips the title heading and a key-facts list.
62
+ */
63
+ function leadExcerpt(markdown, max = 600) {
64
+ const body = markdown.replace(/^#\s[^\n]*\n+/, '');
65
+ const paragraph = body
66
+ .split(/\n\s*\n/)
67
+ .map((part) => part.trim())
68
+ .find((part) => part.length > 0 && !part.startsWith('#') && !part.startsWith('- ')) ??
69
+ body.trim();
70
+ return paragraph.length > max ? `${paragraph.slice(0, max - 1)}…` : paragraph;
71
+ }
72
+ export async function searchUnifiedCatalogs(deps, active, query, opts) {
73
+ if (active.length === 0)
74
+ return [];
75
+ const groups = [];
76
+ const shared = [];
77
+ const keyword = [];
78
+ const byProfile = new Map();
79
+ for (const key of active) {
80
+ const info = deps.catalog(key);
81
+ if (!info)
82
+ continue;
83
+ if (info.semanticSearch === 'shared' && !opts.localModelsOnly)
84
+ shared.push(key);
85
+ else if (info.semanticSearch === 'profile' || info.semanticSearch === 'shared') {
86
+ const group = byProfile.get(info.embedding.id) ?? { profile: info.embedding, keys: [] };
87
+ group.keys.push(key);
88
+ byProfile.set(info.embedding.id, group);
89
+ }
90
+ else
91
+ keyword.push(key);
92
+ }
93
+ if (shared.length > 0) {
94
+ groups.push({
95
+ keys: shared,
96
+ ...(opts.vector ? { vector: Float32Array.from(opts.vector) } : {}),
97
+ });
98
+ }
99
+ for (const group of byProfile.values()) {
100
+ const vector = await deps.embedForProfile(query, group.profile, opts.queryEmbedBudgetMs, opts.localModelsOnly);
101
+ groups.push({ keys: group.keys, ...(vector ? { vector } : {}) });
102
+ }
103
+ if (keyword.length > 0)
104
+ groups.push({ keys: keyword });
105
+ const response = {
106
+ chunks: [],
107
+ documents: [],
108
+ };
109
+ for (const group of groups) {
110
+ const request = {
111
+ query,
112
+ shardBudget: ROUTE_BUDGET_EXPLICIT,
113
+ titleRouteShards: TITLE_ROUTE_EXPLICIT,
114
+ finalK: FINAL_K,
115
+ includeChunkFts: true,
116
+ catalogKeys: group.keys,
117
+ // A pool no deeper than the page asked for: six title hits could
118
+ // never fill a twenty-result browse search.
119
+ docFtsLimit: Math.max(DOC_FTS_MIN, opts.maxResults),
120
+ chunkFtsLimit: Math.max(CHUNK_FTS_MIN, opts.maxResults),
121
+ spatial: opts.spatial,
122
+ };
123
+ let part;
124
+ try {
125
+ part = await deps.host.search(group.vector
126
+ ? { ...request, vector: group.vector, ...(opts.media ? { mediaK: MEDIA_K } : {}) }
127
+ : request);
128
+ }
129
+ catch (error) {
130
+ // A catalog that refuses the query vector (a width the catalog does
131
+ // not store, say) is an embedding failure, not a search failure: the
132
+ // worker forwards only the message, so any vector-arm error falls
133
+ // back to the keyword arms for this group.
134
+ if (!group.vector)
135
+ throw error;
136
+ deps.warn(`[knowledge] semantic search failed for ${group.keys.join(', ')}; keyword-only: ${error instanceof Error ? error.message : String(error)}`);
137
+ part = await deps.host.search(request);
138
+ }
139
+ response.chunks.push(...part.chunks);
140
+ response.documents.push(...part.documents);
141
+ }
142
+ // ── fuse the arms per document ──────────────────────────────────────
143
+ const fused = new Map();
144
+ const bump = (catalogKey, documentId, weight, rank, chunk) => {
145
+ const key = `${catalogKey}\u0000${documentId}`;
146
+ const entry = fused.get(key) ?? { catalogKey, documentId, score: 0 };
147
+ entry.score += weight / (RRF_K + rank);
148
+ // The document's representative chunk: its best vector chunk, else
149
+ // whichever chunk-FTS row named it.
150
+ if (chunk && (!entry.chunk || (chunk.cosine ?? -1) > (entry.chunk.cosine ?? -1))) {
151
+ entry.chunk = chunk;
152
+ }
153
+ fused.set(key, entry);
154
+ };
155
+ // Every text passage the arms found, per document, best vector match first,
156
+ // for `chunksPerDocument`. Fusion still ranks documents, not passages.
157
+ const passagesByDocument = new Map();
158
+ if ((opts.chunksPerDocument ?? 1) > 1) {
159
+ for (const hit of response.chunks
160
+ .filter((h) => !h.media)
161
+ .sort((a, b) => (b.cosine ?? -1) - (a.cosine ?? -1))) {
162
+ const key = `${hit.catalogKey}\u0000${hit.documentId}`;
163
+ const passages = passagesByDocument.get(key) ?? [];
164
+ if (!passages.some((known) => known.chunkUid === hit.chunkUid))
165
+ passages.push(hit);
166
+ passagesByDocument.set(key, passages);
167
+ }
168
+ }
169
+ // Vector arm: one rank per document, in rerank-cosine order. A hit under
170
+ // its profile's floor is not evidence at all — KNN always returns its
171
+ // nearest rows, and a rank-0 row fuses to relevance 1.0 however far away
172
+ // it is (see vector-floors.ts).
173
+ const textChunks = response.chunks.filter((h) => !h.media);
174
+ const mediaChunks = response.chunks.filter((h) => h.media);
175
+ const seenVector = new Set();
176
+ let vectorRank = 0;
177
+ for (const hit of textChunks
178
+ .filter((h) => h.cosine !== undefined)
179
+ .sort((a, b) => (b.cosine ?? 0) - (a.cosine ?? 0))) {
180
+ const key = `${hit.catalogKey}\u0000${hit.documentId}`;
181
+ if (seenVector.has(key))
182
+ continue;
183
+ const cosine = hit.cosine ?? 0;
184
+ const floor = deps.vectorFloorFor(hit.catalogKey);
185
+ if (floor !== null && cosine < floor)
186
+ continue;
187
+ seenVector.add(key);
188
+ bump(hit.catalogKey, hit.documentId, ARM_WEIGHTS.vector, vectorRank++, hit);
189
+ const entry = fused.get(key);
190
+ if (entry && floor !== null && cosine > (entry.similarity ?? Number.NEGATIVE_INFINITY)) {
191
+ entry.similarity = cosine;
192
+ }
193
+ }
194
+ // Chunk-body FTS arm: BM25 order as the shards returned it, ranked per catalog.
195
+ const chunkFtsRank = new Map();
196
+ const seenChunkFts = new Set();
197
+ for (const hit of textChunks.filter((h) => h.cosine === undefined)) {
198
+ const key = `${hit.catalogKey}\u0000${hit.documentId}`;
199
+ if (seenChunkFts.has(key))
200
+ continue;
201
+ seenChunkFts.add(key);
202
+ const rank = chunkFtsRank.get(hit.catalogKey) ?? 0;
203
+ chunkFtsRank.set(hit.catalogKey, rank + 1);
204
+ bump(hit.catalogKey, hit.documentId, ARM_WEIGHTS.chunkFts, rank, hit);
205
+ }
206
+ // Doc-title FTS arm: exact-name evidence, ranked per catalog by the host.
207
+ for (const doc of response.documents) {
208
+ bump(doc.catalogKey, doc.documentId, ARM_WEIGHTS.docFts, doc.rank);
209
+ }
210
+ // Media rows: their own vector arm per modality (a modality with no
211
+ // measured floor gives no vector evidence — nearest-neighbour search
212
+ // always returns some image, and a media row has no query words to be
213
+ // grounded in) and a caption FTS arm.
214
+ const mediaFused = new Map();
215
+ const bumpMedia = (hit, weight, rank) => {
216
+ const key = `${hit.catalogKey}\u0000${hit.chunkUid}`;
217
+ const entry = mediaFused.get(key) ?? {
218
+ catalogKey: hit.catalogKey,
219
+ documentId: hit.documentId,
220
+ score: 0,
221
+ chunk: hit,
222
+ };
223
+ entry.score += weight / (RRF_K + rank);
224
+ if ((hit.cosine ?? -1) > (entry.chunk?.cosine ?? -1))
225
+ entry.chunk = hit;
226
+ mediaFused.set(key, entry);
227
+ };
228
+ const mediaVectorRank = new Map();
229
+ for (const hit of mediaChunks
230
+ .filter((h) => h.cosine !== undefined)
231
+ .sort((a, b) => (b.cosine ?? 0) - (a.cosine ?? 0))) {
232
+ const modality = hit.media?.modality;
233
+ const floor = deps.vectorFloorFor(hit.catalogKey, modality);
234
+ if (floor === null || (hit.cosine ?? 0) < floor)
235
+ continue;
236
+ const rank = mediaVectorRank.get(modality) ?? 0;
237
+ mediaVectorRank.set(modality, rank + 1);
238
+ bumpMedia(hit, MEDIA_ARM_WEIGHTS.vector, rank);
239
+ const entry = mediaFused.get(`${hit.catalogKey}\u0000${hit.chunkUid}`);
240
+ if (entry)
241
+ entry.similarity = Math.max(entry.similarity ?? -1, hit.cosine ?? 0);
242
+ }
243
+ let mediaFtsRank = 0;
244
+ for (const hit of mediaChunks.filter((h) => h.cosine === undefined)) {
245
+ bumpMedia(hit, MEDIA_ARM_WEIGHTS.chunkFts, mediaFtsRank++);
246
+ }
247
+ const ordered = [...fused.values(), ...mediaFused.values()].sort((a, b) => b.score - a.score ||
248
+ (b.chunk?.cosine ?? -1) - (a.chunk?.cosine ?? -1) ||
249
+ a.documentId.localeCompare(b.documentId));
250
+ const matches = new Map([...response.documents, ...response.chunks]
251
+ .filter((hit) => hit.matchedLocation)
252
+ .map((hit) => [
253
+ `${hit.catalogKey}\u0000${hit.documentId}`,
254
+ { distanceMeters: hit.distanceMeters, matchedLocation: hit.matchedLocation },
255
+ ]));
256
+ const seenSpatial = new Set();
257
+ const out = [];
258
+ const perCatalogCount = new Map();
259
+ const capPerCatalog = !opts.spatial && active.length > 1;
260
+ const mediaPerDocument = new Map();
261
+ let mediaResults = 0;
262
+ for (const entry of ordered) {
263
+ const info = deps.catalog(entry.catalogKey);
264
+ if (!info)
265
+ continue;
266
+ if (entry.chunk?.media) {
267
+ const docKey = `${entry.catalogKey}\u0000${entry.documentId}`;
268
+ const forDocument = mediaPerDocument.get(docKey) ?? 0;
269
+ if (mediaResults >= MEDIA_RESULT_CAP || forDocument >= MEDIA_PER_DOCUMENT)
270
+ continue;
271
+ const evidence = entry.similarity !== undefined
272
+ ? { arm: 'vector', similarity: entry.similarity }
273
+ : { arm: 'fts' };
274
+ mediaPerDocument.set(docKey, forDocument + 1);
275
+ mediaResults++;
276
+ out.push(knowledgeChunkResult(info, entry.chunk, fusedRankRelevance(out.length), evidence));
277
+ if (out.length >= Math.max(10, opts.maxResults))
278
+ break;
279
+ continue;
280
+ }
281
+ const count = perCatalogCount.get(entry.catalogKey) ?? 0;
282
+ if (capPerCatalog && count >= PER_CATALOG_CAP)
283
+ continue;
284
+ const spatialIdentity = spatialDocumentIdentity(info.ref.publisherId, info.ref.catalogId, entry.documentId);
285
+ if (opts.spatial && seenSpatial.has(spatialIdentity))
286
+ continue;
287
+ const relevance = fusedRankRelevance(out.length);
288
+ // `vector` only when the hit cleared a measured floor; everything else
289
+ // is lexical evidence, which proactive injection asks to be grounded.
290
+ const evidence = entry.similarity !== undefined
291
+ ? { arm: 'vector', similarity: entry.similarity }
292
+ : { arm: 'fts' };
293
+ const result = entry.chunk
294
+ ? knowledgeChunkResult(info, entry.chunk, relevance, evidence)
295
+ : await knowledgeDocumentResult(deps.host, info, entry.documentId, relevance, evidence);
296
+ if (!result)
297
+ continue;
298
+ perCatalogCount.set(entry.catalogKey, count + 1);
299
+ seenSpatial.add(spatialIdentity);
300
+ const docKey = `${entry.catalogKey}\u0000${entry.documentId}`;
301
+ out.push({ ...result, ...matches.get(docKey) });
302
+ if (out.length >= Math.max(10, opts.maxResults))
303
+ break;
304
+ // The document's other passages, each labelled by its own evidence: a
305
+ // sibling is vector evidence only if it clears the floor by itself.
306
+ const siblings = (passagesByDocument.get(docKey) ?? [])
307
+ .filter((hit) => hit.chunkUid !== entry.chunk?.chunkUid)
308
+ .slice(0, Math.max(0, (opts.chunksPerDocument ?? 1) - 1));
309
+ const floor = deps.vectorFloorFor(entry.catalogKey);
310
+ for (const sibling of siblings) {
311
+ const own = floor !== null && sibling.cosine !== undefined && sibling.cosine >= floor
312
+ ? { arm: 'vector', similarity: sibling.cosine }
313
+ : { arm: 'fts' };
314
+ out.push(knowledgeChunkResult(info, sibling, fusedRankRelevance(out.length), own));
315
+ if (out.length >= Math.max(10, opts.maxResults))
316
+ break;
317
+ }
318
+ if (out.length >= Math.max(10, opts.maxResults))
319
+ break;
320
+ }
321
+ return out;
322
+ }
323
+ /** A document the lexical arms named but no chunk carries: cite it with its lead paragraph. */
324
+ export async function knowledgeDocumentResult(host, info, documentId, relevance, evidence) {
325
+ const doc = await host.getDocument(info.key, documentId).catch(() => null);
326
+ if (!doc)
327
+ return null;
328
+ const snippet = leadExcerpt(doc.markdown) || doc.summary || '';
329
+ return {
330
+ kind: 'knowledge',
331
+ id: `knowledge:${info.ref.catalogId}:doc:${documentId}`,
332
+ title: doc.title,
333
+ subtitle: `${info.name} · ${info.topicNames.get(doc.topicId) ?? doc.topicId}`,
334
+ ...(snippet ? { snippet } : {}),
335
+ retrievalSource: 'knowledge',
336
+ catalogId: info.ref.catalogId,
337
+ catalogVersion: info.ref.version,
338
+ documentId,
339
+ topicPath: [info.topicNames.get(doc.topicId) ?? doc.topicId],
340
+ uri: formatKnowledgeUri({
341
+ publisherId: info.ref.publisherId,
342
+ catalogId: info.ref.catalogId,
343
+ documentId,
344
+ }),
345
+ ...(doc.sourceUrl ? { sourceUrl: doc.sourceUrl } : {}),
346
+ ...evidence,
347
+ ...scoreResult('knowledge', relevance),
348
+ };
349
+ }
350
+ export function knowledgeChunkResult(info, hit, relevance, evidence) {
351
+ return {
352
+ kind: 'knowledge',
353
+ id: `knowledge:${info.ref.catalogId}:${hit.chunkUid}`,
354
+ title: hit.headingPath.length > 0 ? `${hit.title} › ${hit.headingPath.join(' › ')}` : hit.title,
355
+ subtitle: info.name,
356
+ snippet: hit.text,
357
+ retrievalSource: 'knowledge',
358
+ catalogId: info.ref.catalogId,
359
+ catalogVersion: info.ref.version,
360
+ documentId: hit.documentId,
361
+ uri: formatKnowledgeUri({
362
+ publisherId: info.ref.publisherId,
363
+ catalogId: info.ref.catalogId,
364
+ documentId: hit.documentId,
365
+ fragment: { chunk: hit.chunkUid },
366
+ }),
367
+ line: hit.lineStart,
368
+ lineEnd: hit.lineEnd,
369
+ ...(hit.media ? { media: hit.media } : {}),
370
+ ...evidence,
371
+ ...scoreResult('knowledge', relevance),
372
+ };
373
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { Worker } from 'node:worker_threads';
9
9
  import { createLogger } from '@bendyline/gezel';
10
+ import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
10
11
  import { findServiceWorkerEntry } from '../utils/service-worker-entry.js';
11
12
  import { createInProcessCatalogHost } from './catalog-host.js';
12
13
  const log = createLogger('knowledge');
@@ -63,6 +64,7 @@ export function createWorkerCatalogHost() {
63
64
  return null;
64
65
  }
65
66
  try {
67
+ disableOnnxRuntimeTelemetry();
66
68
  const w = new Worker(entry);
67
69
  w.on('message', (msg) => {
68
70
  const p = pending.get(msg.id);
@@ -1,12 +1,21 @@
1
+ import { innermostErrorReason } from '@bendyline/gezel';
1
2
  import { IdentityResponseSchema, certFingerprintFromPem, fingerprintOfPublicKeyPem, verifyIdentitySignature, } from '../remotes/identity.js';
2
3
  import { createPinnedFetch } from '../remotes/pinned-fetch.js';
3
4
  const MACHINE_REMOTE_ID = 'this-machine';
4
5
  export async function inspectMachineRuntime(runtime) {
5
6
  const fetchImpl = runtime.cert ? createPinnedFetch(runtime.cert) : fetch;
6
7
  try {
7
- const response = await fetchImpl(`${runtime.baseUrl}/v1/identity`, {
8
- signal: AbortSignal.timeout(5_000),
9
- });
8
+ let response;
9
+ try {
10
+ response = await fetchImpl(`${runtime.baseUrl}/v1/identity`, {
11
+ signal: AbortSignal.timeout(5_000),
12
+ });
13
+ }
14
+ catch (error) {
15
+ // Bare, this surfaced as `fetch failed` in chat bubbles that never
16
+ // mentioned a machine engine at all.
17
+ throw new Error(`Gezel's machine engine at ${runtime.baseUrl} isn't answering (${innermostErrorReason(error)})`, { cause: error });
18
+ }
10
19
  if (!response.ok)
11
20
  throw new Error(`identity returned HTTP ${response.status}`);
12
21
  const identity = IdentityResponseSchema.parse(await response.json());
@@ -15,6 +15,7 @@
15
15
  import { createHash } from 'node:crypto';
16
16
  import { createLogger, embeddingProfileArtifacts, } from '@bendyline/gezel';
17
17
  import { EmbedderUnavailableError, KNOWLEDGE_EMBEDDING_PROFILES, createProfileEmbedder, resolveTransformersModelOptions, verifyProfileArtifacts, } from '@bendyline/gezel-knowledge';
18
+ import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
18
19
  import { HF_CACHE_DIR_ENV, ModelFileDownloadError, TRANSFORMERS_MODULE, ensureVerifiedModelFile, isCorruptTransformersCacheFailure, isMissingModule, loadTransformersModelWithCacheRecovery, pinTransformersCacheDir, } from '../transformers-cache.js';
19
20
  export const log = createLogger('memory');
20
21
  /**
@@ -236,6 +237,7 @@ export async function loadPipeline() {
236
237
  // and throws "Unable to get model file path or buffer". The dir is
237
238
  // passed via env so this works in the embed worker thread too, which
238
239
  // inherits `process.env` but has no other view of the gezel home.
240
+ disableOnnxRuntimeTelemetry();
239
241
  const cacheDir = process.env[HF_CACHE_DIR_ENV];
240
242
  if (cacheDir)
241
243
  await pinTransformersCacheDir(cacheDir);
@@ -348,6 +350,7 @@ async function verifyDaemonPipeline() {
348
350
  }
349
351
  }
350
352
  async function defaultTransformersCacheDir() {
353
+ disableOnnxRuntimeTelemetry();
351
354
  const { env } = await import('@huggingface/transformers');
352
355
  // 4.x types it nullable: null only where there is no filesystem cache.
353
356
  return env.cacheDir ?? '';
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { Worker } from 'node:worker_threads';
21
21
  import { createLogger, sameVectorSpace } from '@bendyline/gezel';
22
+ import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
22
23
  import { retireModelWorker } from '../utils/retire-model-worker.js';
23
24
  import { findServiceWorkerEntry } from '../utils/service-worker-entry.js';
24
25
  import { ModelWorkCancelledError, PipelineLoadError, cancelInFlightModelWork, daemonEmbedderPin, daemonEmbedderVerified, passageInstruction, queryInstruction, runEmbed, runProfileQueryEmbed, } from './embed-core.js';
@@ -124,6 +125,7 @@ function ensureWorker() {
124
125
  return null;
125
126
  }
126
127
  try {
128
+ disableOnnxRuntimeTelemetry();
127
129
  const w = new Worker(entry);
128
130
  w.on('message', onMessage);
129
131
  w.on('error', (err) => onWorkerDown(err instanceof Error ? err.message : String(err)));
@@ -11,12 +11,12 @@
11
11
  * The video half lives with the video engine in the service, which shares
12
12
  * the helpers here.
13
13
  */
14
- import { totalmem } from 'node:os';
15
14
  import { MEDIA_MODEL_WONT_RUN_CODE, assessMediaFit, createLogger, imageMemoryRequirement, imageModelDefaultSide, resolveImageSize, } from '@bendyline/gezel';
16
15
  import { isCudaArchivePath } from '@bendyline/gezel/native';
17
16
  import { sdServerLoadProfile } from '../providers/image/sd-cpp.js';
18
17
  import { configuredEngineMemoryBytes, staticCapacityCeilings, } from '../providers/native/device-capacity.js';
19
18
  import { measuredCapacityBudget } from '../providers/native/measured-budget.js';
19
+ import { totalMemoryBytes } from '../system/host-memory.js';
20
20
  import { detectMemoryProfileCached } from '../system/memory.js';
21
21
  import { imageCatalogWeightFiles, sdCppMainComponent } from './weight-facts.js';
22
22
  const log = createLogger('media-fit');
@@ -63,7 +63,7 @@ export function imageMediaPool(kind, host = {
63
63
  /** The ceilings the device ledger applies on this machine, automatic and under the person's limit. */
64
64
  export async function mediaCapacity(pool, readConfig) {
65
65
  const budget = await measuredCapacityBudget();
66
- const ram = totalmem();
66
+ const ram = totalMemoryBytes();
67
67
  const auto = staticCapacityCeilings(budget, ram);
68
68
  const configured = configuredEngineMemoryBytes(await readConfig().catch(() => ({})));
69
69
  const limited = configured === undefined ? undefined : staticCapacityCeilings(budget, ram, configured);
@@ -34,3 +34,8 @@ export declare function localSdCppWeightFiles(model: {
34
34
  path: string;
35
35
  }>;
36
36
  }): Promise<MediaWeightFile[]>;
37
+ /** Weight files on disk, priced from their headers where they are safetensors. */
38
+ export declare function localWeightFiles(entries: ReadonlyArray<{
39
+ component: string;
40
+ path: string;
41
+ }>): Promise<MediaWeightFile[]>;
@@ -148,10 +148,13 @@ export function imageCatalogWeightFiles(manifest) {
148
148
  }
149
149
  /** The weight files of an installed sd-cpp model, headers read from disk. */
150
150
  export async function localSdCppWeightFiles(model) {
151
- const entries = [
151
+ return localWeightFiles([
152
152
  { component: sdCppMainComponent(model.weightsKind), path: model.weightsPath },
153
153
  ...model.auxiliaryFiles.map((a) => ({ component: a.role, path: a.path })),
154
- ];
154
+ ]);
155
+ }
156
+ /** Weight files on disk, priced from their headers where they are safetensors. */
157
+ export async function localWeightFiles(entries) {
155
158
  const out = [];
156
159
  for (const entry of entries) {
157
160
  const format = weightFileFormat(entry.path);
@@ -123,7 +123,7 @@ export async function buildDs4Provider(opts) {
123
123
  // This tier assumes DeepSeek-V4's small resident footprint. A model whose
124
124
  // non-routed weights are much larger caps it further via the catalog's
125
125
  // `ds4.maxLaunchCtx`; see `resolveDs4LaunchCtx` below.
126
- const totalRamBytes = (await import('node:os')).totalmem();
126
+ const totalRamBytes = (await import('../../system/host-memory.js')).totalMemoryBytes();
127
127
  const totalRamGb = totalRamBytes / 1024 ** 3;
128
128
  const ramTieredCtx = ds4RamTieredContext(totalRamBytes);
129
129
  const ds4ConstrainedToolNoSignalMs = (() => {
@@ -1,5 +1,5 @@
1
- import { totalmem } from 'node:os';
2
1
  import { DS4_FULL_RESIDENCY_HEADROOM_BYTES, LLAMA_CPP_VISION_COMPUTE_BYTES, LLAMA_CPP_WEIGHTS_MULTIPLIER, ds4FitsFullResidency, } from '@bendyline/gezel';
2
+ import { totalMemoryBytes } from '../../system/host-memory.js';
3
3
  const GB = 1024 ** 3;
4
4
  export { DS4_FULL_RESIDENCY_HEADROOM_BYTES };
5
5
  /**
@@ -43,7 +43,7 @@ export function ds4BaseResidentBytes(opts) {
43
43
  export function canUseDs4FullResidency(opts = {}) {
44
44
  return ds4FitsFullResidency({
45
45
  modelSizeBytes: opts.modelSizeBytes,
46
- totalRamBytes: opts.totalRamBytes ?? totalmem(),
46
+ totalRamBytes: opts.totalRamBytes ?? totalMemoryBytes(),
47
47
  companionBytes: opts.companionBytes,
48
48
  platform: opts.platform ?? process.platform,
49
49
  arch: opts.arch ?? process.arch,
@@ -82,7 +82,7 @@ export function shouldUseDs4SsdStreaming(opts = {}) {
82
82
  * state + the cache cannot consume the runtime/OS reserve.
83
83
  */
84
84
  export function planDs4ExpertCache(opts) {
85
- const totalRamBytes = opts.totalRamBytes ?? totalmem();
85
+ const totalRamBytes = opts.totalRamBytes ?? totalMemoryBytes();
86
86
  const catalogCacheBytes = opts.catalogCacheBytes ?? 0;
87
87
  const fixedResidentBytes = Math.max(0, (opts.catalogResidentBytes ?? 0) - catalogCacheBytes);
88
88
  const fallbackGb = totalRamBytes >= 120 * GB ? 64 : totalRamBytes >= 88 * GB ? 48 : 32;
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Per-model img2img capability resolution for the local sd-cpp engine.
2
+ * Per-model image-editing capability for the local sd-cpp engine: whether a
3
+ * model re-noises a source (img2img), reads sources as references beside an
4
+ * instruction, or cannot use one. The img2img half first.
3
5
  *
4
6
  * stable-diffusion.cpp's `/sdapi/v1/img2img` endpoint accepts
5
7
  * `init_images` for any loaded model, but whether the *architecture*
@@ -37,4 +39,37 @@ export interface Img2ImgSupportVerdict {
37
39
  /** Human-readable reason, set only when unsupported. */
38
40
  reason?: string;
39
41
  }
42
+ /**
43
+ * Models that edit by reading source pictures as references beside an
44
+ * instruction (sd-server's `extra_images`), rather than re-noising one. The
45
+ * same ladder as above: a catalog `referenceImages` declaration persisted at
46
+ * pull time wins, then this map, which is how installs that predate the
47
+ * field learn it.
48
+ *
49
+ * FLUX.2 Klein: verified on the pinned engine (native sd-cpp master-975) on
50
+ * an RTX Spark, 2026-10-27, recoloring, adding an object and composing two
51
+ * sources at 4 steps in 12 s.
52
+ */
53
+ export declare const MODEL_REFERENCE_EDIT_SUPPORT: Map<string, {
54
+ maxImages: number;
55
+ editOnly?: boolean;
56
+ }>;
57
+ export interface ImageEditCapabilityInput extends Img2ImgSupportInput {
58
+ /** Explicit `referenceImages` from catalog/installed metadata. */
59
+ referenceImages?: {
60
+ max: number;
61
+ } | undefined;
62
+ /** Explicit `editOnly` from catalog/installed metadata. */
63
+ editOnly?: boolean | undefined;
64
+ }
65
+ export interface ImageEditCapability {
66
+ /** Absent when the model cannot use a source picture at all. */
67
+ editMode?: 'reference' | 'img2img';
68
+ /** Sources one edit takes: the reference limit, or 1 for img2img. */
69
+ maxSourceImages?: number;
70
+ editOnly: boolean;
71
+ /** Why the model cannot edit; set only when `editMode` is absent. */
72
+ reason?: string;
73
+ }
74
+ export declare function resolveImageEditCapability(input: ImageEditCapabilityInput): ImageEditCapability;
40
75
  export declare function resolveImg2ImgSupport(input: Img2ImgSupportInput): Img2ImgSupportVerdict;
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Per-model img2img capability resolution for the local sd-cpp engine.
2
+ * Per-model image-editing capability for the local sd-cpp engine: whether a
3
+ * model re-noises a source (img2img), reads sources as references beside an
4
+ * instruction, or cannot use one. The img2img half first.
3
5
  *
4
6
  * stable-diffusion.cpp's `/sdapi/v1/img2img` endpoint accepts
5
7
  * `init_images` for any loaded model, but whether the *architecture*
@@ -44,6 +46,33 @@ export const MODEL_IMG2IMG_SUPPORT = new Map([
44
46
  ['krea-2-turbo-q6', false],
45
47
  ['krea-2-turbo-q8', false],
46
48
  ]);
49
+ /**
50
+ * Models that edit by reading source pictures as references beside an
51
+ * instruction (sd-server's `extra_images`), rather than re-noising one. The
52
+ * same ladder as above: a catalog `referenceImages` declaration persisted at
53
+ * pull time wins, then this map, which is how installs that predate the
54
+ * field learn it.
55
+ *
56
+ * FLUX.2 Klein: verified on the pinned engine (native sd-cpp master-975) on
57
+ * an RTX Spark, 2026-10-27, recoloring, adding an object and composing two
58
+ * sources at 4 steps in 12 s.
59
+ */
60
+ export const MODEL_REFERENCE_EDIT_SUPPORT = new Map([
61
+ ['flux-2-klein-4b-q4', { maxImages: 3 }],
62
+ ['qwen-image-edit-2511-q4', { maxImages: 3, editOnly: true }],
63
+ ]);
64
+ export function resolveImageEditCapability(input) {
65
+ const assessed = input.modelId ? MODEL_REFERENCE_EDIT_SUPPORT.get(input.modelId) : undefined;
66
+ const editOnly = input.editOnly ?? assessed?.editOnly ?? false;
67
+ const maxImages = input.referenceImages?.max ?? assessed?.maxImages;
68
+ if (maxImages !== undefined) {
69
+ return { editMode: 'reference', maxSourceImages: maxImages, editOnly };
70
+ }
71
+ const img2img = resolveImg2ImgSupport(input);
72
+ if (img2img.supported)
73
+ return { editMode: 'img2img', maxSourceImages: 1, editOnly };
74
+ return { editOnly, ...(img2img.reason ? { reason: img2img.reason } : {}) };
75
+ }
47
76
  export function resolveImg2ImgSupport(input) {
48
77
  if (input.explicit !== undefined) {
49
78
  return input.explicit
@@ -21,6 +21,7 @@ export interface LocalImageEngineOptions {
21
21
  export declare class LocalImageEngine {
22
22
  private readonly opts;
23
23
  readonly pulls: ImageModelPullRegistry;
24
+ readonly loraPulls: ImageModelPullRegistry;
24
25
  private readonly lifecycle;
25
26
  constructor(opts: LocalImageEngineOptions);
26
27
  /** Whether this host can render at all: an engine binary, a server URL, or the mock. */