@bendyline/gezel-inference 1.0.7 → 1.0.9
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.
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
- package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +5 -4
- package/dist/engines/electron-native-reuse.js +1 -1
- package/dist/engines/native-file-manifest.json +12344 -698
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +40 -36
- package/dist/engines/resolver.d.ts +18 -17
- package/dist/engines/resolver.js +36 -1
- package/dist/http/model-inventory-cache.d.ts +1 -1
- package/dist/http/routes/engines.js +3 -3
- package/dist/http/routes/v1-images.d.ts +2 -1
- package/dist/http/routes/v1-images.js +36 -9
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- package/dist/models/catalog-drift.d.ts +2 -0
- package/dist/models/catalog-drift.js +1 -1
- package/dist/models/catalog-payload-identity.d.ts +2 -2
- package/dist/models/catalog-payload-identity.js +10 -5
- package/dist/providers/active-install-message.js +1 -1
- package/dist/providers/catalog-model-config.d.ts +28 -0
- package/dist/providers/ds4/build-provider.js +35 -13
- package/dist/providers/ds4/residency.d.ts +77 -0
- package/dist/providers/ds4/residency.js +87 -6
- package/dist/providers/image/sd-cpp.js +19 -11
- package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
- package/dist/providers/llama-cpp/backend-health.js +6 -3
- package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
- package/dist/providers/llama-cpp/build-provider.js +168 -29
- package/dist/providers/llama-cpp/devices.js +4 -1
- package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
- package/dist/providers/llama-cpp/engine-flags.js +13 -8
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
- package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
- package/dist/providers/llama-cpp/models.d.ts +10 -0
- package/dist/providers/llama-cpp/models.js +64 -10
- package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
- package/dist/providers/llama-cpp/offload-planner.js +1 -1
- package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
- package/dist/providers/llama-cpp/spec-draft.js +113 -24
- package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +28 -16
- package/dist/providers/mlx/python/spec_decode.py +23 -2
- package/dist/providers/mlx/python/tool_args_json.py +59 -21
- package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
- package/dist/providers/native/device-capacity-ledger.js +35 -20
- package/dist/providers/native/device-capacity.d.ts +4 -0
- package/dist/providers/native/device-capacity.js +5 -0
- package/dist/providers/native/local-engine-runtime.d.ts +16 -1
- package/dist/providers/native/local-engine-runtime.js +142 -39
- package/dist/providers/native/process-identity.d.ts +38 -0
- package/dist/providers/native/process-identity.js +86 -0
- package/dist/providers/native/supervisor.d.ts +8 -1
- package/dist/providers/native/supervisor.js +16 -7
- package/dist/providers/types.d.ts +4 -0
- package/dist/remotes/types.d.ts +1 -1
- package/dist/runtime-lock.d.ts +3 -27
- package/dist/runtime-lock.js +7 -80
- package/package.json +4 -4
|
@@ -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
|
+
}
|
|
@@ -36,6 +36,8 @@ export interface EvaluateCatalogDriftOptions {
|
|
|
36
36
|
installedVersion?: string;
|
|
37
37
|
installed: InstalledPayloadRecord;
|
|
38
38
|
manifest: ChatModelManifest;
|
|
39
|
+
/** The backend a llama.cpp copy runs on. Defaults to the process's own. */
|
|
40
|
+
llamaCppBackend?: string | undefined;
|
|
39
41
|
/**
|
|
40
42
|
* False for models resolved out of a read-only overlay (the machine asset
|
|
41
43
|
* store) and while an install is in flight for this id — the verdict still
|
|
@@ -30,7 +30,7 @@ export async function evaluateCatalogDrift(opts) {
|
|
|
30
30
|
if (!opts.installedVersion || !currentVersion || opts.installedVersion === currentVersion) {
|
|
31
31
|
return { updateAvailable: false };
|
|
32
32
|
}
|
|
33
|
-
const catalog = describeCatalogPayload(opts.manifest, opts.engine);
|
|
33
|
+
const catalog = describeCatalogPayload(opts.manifest, opts.engine, opts.llamaCppBackend);
|
|
34
34
|
if (!catalog) {
|
|
35
35
|
// The catalog no longer describes this engine's payload at all (a source
|
|
36
36
|
// block was pulled). Nothing to download, so nothing to offer.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* over what the install already recorded, and its answers steer UI, never
|
|
31
31
|
* trust.
|
|
32
32
|
*/
|
|
33
|
-
import type
|
|
33
|
+
import { type ChatModelManifest, type GezmodelEngine } from '@bendyline/gezel';
|
|
34
34
|
/** One file the catalog expects to land in the model directory. */
|
|
35
35
|
export interface CatalogPayloadFile {
|
|
36
36
|
/** Name as it lands on disk ('/'-joined for nested files, as in `fileSha256`). */
|
|
@@ -103,5 +103,5 @@ export declare function catalogPayloadFingerprint(payload: CatalogPayload): stri
|
|
|
103
103
|
* The single place that knows each source block's shape, so callers compare
|
|
104
104
|
* like for like without re-deriving filenames.
|
|
105
105
|
*/
|
|
106
|
-
export declare function describeCatalogPayload(manifest: ChatModelManifest, engine: GezmodelEngine): CatalogPayload | null;
|
|
106
|
+
export declare function describeCatalogPayload(manifest: ChatModelManifest, engine: GezmodelEngine, llamaCppBackend?: string | undefined): CatalogPayload | null;
|
|
107
107
|
export declare function comparePayloadIdentity(opts: ComparePayloadOptions): Promise<PayloadIdentityResult>;
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
import { createHash } from 'node:crypto';
|
|
34
34
|
import { stat } from 'node:fs/promises';
|
|
35
35
|
import { basename, join } from 'node:path';
|
|
36
|
+
import { llamaCppDraftModelApplies, } from '@bendyline/gezel';
|
|
37
|
+
import { currentLlamaCppBackend } from '../providers/llama-cpp/backend-health.js';
|
|
36
38
|
/**
|
|
37
39
|
* Stable hash of a catalog payload description. Deliberately excludes
|
|
38
40
|
* `revision`: it is a fetch coordinate, and identical hashes mean identical
|
|
@@ -53,7 +55,7 @@ export function catalogPayloadFingerprint(payload) {
|
|
|
53
55
|
* The single place that knows each source block's shape, so callers compare
|
|
54
56
|
* like for like without re-deriving filenames.
|
|
55
57
|
*/
|
|
56
|
-
export function describeCatalogPayload(manifest, engine) {
|
|
58
|
+
export function describeCatalogPayload(manifest, engine, llamaCppBackend = currentLlamaCppBackend()) {
|
|
57
59
|
if (engine === 'mlx') {
|
|
58
60
|
const src = manifest.mlx;
|
|
59
61
|
if (!src)
|
|
@@ -96,10 +98,13 @@ export function describeCatalogPayload(manifest, engine) {
|
|
|
96
98
|
sizeBytes: src.approxSizeBytes,
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
|
-
// Draft companions exist on both engine sources
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
101
|
+
// Draft companions exist on both engine sources, so read them off the
|
|
102
|
+
// already engine-selected `src`. A llama.cpp drafter limited to some backends
|
|
103
|
+
// is no part of this install elsewhere: counting it would offer every other
|
|
104
|
+
// machine an update for a file it never downloads.
|
|
105
|
+
const draftApplies = engine !== 'llama-cpp' ||
|
|
106
|
+
llamaCppDraftModelApplies(manifest.llamaCpp?.draftModel, llamaCppBackend);
|
|
107
|
+
if (src.draftModel && draftApplies) {
|
|
103
108
|
files.push({
|
|
104
109
|
name: basename(src.draftModel.filename),
|
|
105
110
|
sha256: src.draftModel.sha256,
|
|
@@ -37,7 +37,7 @@ export function notReadyError(message, code) {
|
|
|
37
37
|
*/
|
|
38
38
|
export function noModelYetMessage(engineLabel, active) {
|
|
39
39
|
if (!active) {
|
|
40
|
-
return `${engineLabel}: no model is installed yet. Download one from Settings → Artificial
|
|
40
|
+
return `${engineLabel}: no model is installed yet. Download one from Settings → Artificial intelligence, then try again.`;
|
|
41
41
|
}
|
|
42
42
|
if (active.phase !== 'downloading') {
|
|
43
43
|
const verb = active.phase === 'verifying' ? 'being verified' : 'being prepared';
|
|
@@ -59,5 +59,33 @@ export declare function resolveCatalogLlamaCppEngineConfig(catalog: CatalogServi
|
|
|
59
59
|
mtp?: boolean | undefined;
|
|
60
60
|
draftModelId?: string | undefined;
|
|
61
61
|
nMax?: number | undefined;
|
|
62
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
63
|
+
byBackend?: {
|
|
64
|
+
cuda?: {
|
|
65
|
+
type?: "none" | "draft-mtp" | "draft-eagle3" | "draft-dflash" | "draft-simple" | "ngram-mod" | "ngram-simple" | "ngram-map-k" | "ngram-map-k4v" | "ngram-cache" | undefined;
|
|
66
|
+
nMax?: number | undefined;
|
|
67
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
68
|
+
} | undefined;
|
|
69
|
+
rocm?: {
|
|
70
|
+
type?: "none" | "draft-mtp" | "draft-eagle3" | "draft-dflash" | "draft-simple" | "ngram-mod" | "ngram-simple" | "ngram-map-k" | "ngram-map-k4v" | "ngram-cache" | undefined;
|
|
71
|
+
nMax?: number | undefined;
|
|
72
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
73
|
+
} | undefined;
|
|
74
|
+
vulkan?: {
|
|
75
|
+
type?: "none" | "draft-mtp" | "draft-eagle3" | "draft-dflash" | "draft-simple" | "ngram-mod" | "ngram-simple" | "ngram-map-k" | "ngram-map-k4v" | "ngram-cache" | undefined;
|
|
76
|
+
nMax?: number | undefined;
|
|
77
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
78
|
+
} | undefined;
|
|
79
|
+
metal?: {
|
|
80
|
+
type?: "none" | "draft-mtp" | "draft-eagle3" | "draft-dflash" | "draft-simple" | "ngram-mod" | "ngram-simple" | "ngram-map-k" | "ngram-map-k4v" | "ngram-cache" | undefined;
|
|
81
|
+
nMax?: number | undefined;
|
|
82
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
cpu?: {
|
|
85
|
+
type?: "none" | "draft-mtp" | "draft-eagle3" | "draft-dflash" | "draft-simple" | "ngram-mod" | "ngram-simple" | "ngram-map-k" | "ngram-map-k4v" | "ngram-cache" | undefined;
|
|
86
|
+
nMax?: number | undefined;
|
|
87
|
+
sampling?: "greedy" | "probabilistic" | undefined;
|
|
88
|
+
} | undefined;
|
|
89
|
+
} | undefined;
|
|
62
90
|
} | undefined;
|
|
63
91
|
} | undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename, dirname, join } from 'node:path';
|
|
2
|
-
import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, } from '@bendyline/gezel';
|
|
2
|
+
import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, ds4SsdStreamingSupported, } from '@bendyline/gezel';
|
|
3
3
|
import { gezelPaths } from '@bendyline/gezel/paths';
|
|
4
4
|
import { LlamaCppProvider } from '../llama-cpp/index.js';
|
|
5
5
|
import { minViableLocalContextTokens, pressureIdleGraceMs } from '../native/capacity-broker.js';
|
|
@@ -282,7 +282,7 @@ export async function buildDs4Provider(opts) {
|
|
|
282
282
|
// build). `shouldUseDs4SsdStreaming` still refuses a model that cannot fit,
|
|
283
283
|
// which is what keeps the flip safe: overriding that on a 90.89 GiB build
|
|
284
284
|
// OOM-killed the engine mid-load on a 121.63 GiB host.
|
|
285
|
-
const { ds4VisionResidentBytes, planDs4ExpertCache, shouldUseDs4SsdStreaming } = await import('./residency.js');
|
|
285
|
+
const { ds4VisionResidentBytes, planDs4ExpertCache, planDs4MeasuredLaunch, shouldUseDs4SsdStreaming, } = await import('./residency.js');
|
|
286
286
|
let visionEncoderSizeBytes = installedModel?.visionEncoderSizeBytes;
|
|
287
287
|
if (vision.enabled && vision.visionEncoderPath && !visionEncoderSizeBytes) {
|
|
288
288
|
const { stat: statVisionEncoder } = await import('node:fs/promises');
|
|
@@ -291,11 +291,10 @@ export async function buildDs4Provider(opts) {
|
|
|
291
291
|
.catch(() => undefined);
|
|
292
292
|
}
|
|
293
293
|
const visionResidentBytes = ds4VisionResidentBytes(vision.enabled ? visionEncoderSizeBytes : undefined);
|
|
294
|
+
const streamingSupported = ds4SsdStreamingSupported(ds4Source, process.platform);
|
|
294
295
|
const ssdStreaming = shouldUseDs4SsdStreaming({
|
|
295
296
|
configured: config.ds4SsdStreaming,
|
|
296
|
-
...(
|
|
297
|
-
? { ssdStreamingSupported: ds4Source.ssdStreamingSupported }
|
|
298
|
-
: {}),
|
|
297
|
+
...(streamingSupported !== undefined ? { ssdStreamingSupported: streamingSupported } : {}),
|
|
299
298
|
modelSizeBytes: residentWeightBytes,
|
|
300
299
|
totalRamBytes,
|
|
301
300
|
...(visionResidentBytes > 0 ? { companionBytes: visionResidentBytes } : {}),
|
|
@@ -304,15 +303,30 @@ export async function buildDs4Provider(opts) {
|
|
|
304
303
|
log.warn(`[ds4] ignored unsafe full-residency override for ${effectiveModelId ?? modelPath}; ` +
|
|
305
304
|
`model=${modelSizeBytes ?? 'unknown'} bytes, system=${Math.round(totalRamGb)} GiB`);
|
|
306
305
|
}
|
|
307
|
-
if (config.ds4SsdStreaming === true &&
|
|
308
|
-
log.warn(`[ds4] ignored SSD-streaming override for ${effectiveModelId ?? modelPath}; this model
|
|
306
|
+
if (config.ds4SsdStreaming === true && streamingSupported === false) {
|
|
307
|
+
log.warn(`[ds4] ignored SSD-streaming override for ${effectiveModelId ?? modelPath}; ds4 cannot stream this model's routed experts on this backend`);
|
|
309
308
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
// A measured footprint prices the launch for its mode; a measured streamed
|
|
310
|
+
// footprint also sizes the expert cache against the local-model budget.
|
|
311
|
+
const modelBudgetBytes = ssdStreaming && ds4Source?.streamingMemory
|
|
312
|
+
? await (await import('../native/device-capacity.js')).deviceModelBudgetBytes(config)
|
|
313
|
+
: undefined;
|
|
314
|
+
const measuredLaunchWith = (extraResidentBytes) => planDs4MeasuredLaunch({
|
|
315
|
+
source: ds4Source,
|
|
316
|
+
numCtx,
|
|
317
|
+
ssdStreaming,
|
|
314
318
|
totalRamBytes: totalRamGb * 1024 ** 3,
|
|
319
|
+
budgetBytes: modelBudgetBytes,
|
|
320
|
+
sidecarBytes: visionResidentBytes + extraResidentBytes,
|
|
321
|
+
configuredCacheGb: config.ds4CacheExpertsGb,
|
|
315
322
|
});
|
|
323
|
+
const cachePlanWith = (extraResidentBytes) => measuredLaunchWith(extraResidentBytes)?.cache ??
|
|
324
|
+
planDs4ExpertCache({
|
|
325
|
+
configuredGb: config.ds4CacheExpertsGb,
|
|
326
|
+
catalogCacheBytes: ds4Source?.cacheExpertsBytes,
|
|
327
|
+
catalogResidentBytes: (ds4Source?.residentBytes ?? 0) + visionResidentBytes + extraResidentBytes,
|
|
328
|
+
totalRamBytes: totalRamGb * 1024 ** 3,
|
|
329
|
+
});
|
|
316
330
|
// DSpark speculative decoding. Resolved here, beside the residency decision
|
|
317
331
|
// it depends on: ds4 aborts at startup if an external `--mtp-model` is
|
|
318
332
|
// combined with `--ssd-streaming` for any family but DeepSeek V4.1.
|
|
@@ -386,7 +400,9 @@ export async function buildDs4Provider(opts) {
|
|
|
386
400
|
throw err;
|
|
387
401
|
}
|
|
388
402
|
if (cachePlan.clamped) {
|
|
389
|
-
log.warn(`[ds4] clamped expert cache ${cachePlan.requestedGb} → ${cachePlan.cacheGb} GiB to
|
|
403
|
+
log.warn(`[ds4] clamped expert cache ${cachePlan.requestedGb} → ${cachePlan.cacheGb} GiB to ${cachePlan.limit === 'model-budget'
|
|
404
|
+
? "fit this device's local-model budget"
|
|
405
|
+
: 'preserve system headroom'}`);
|
|
390
406
|
}
|
|
391
407
|
const cacheExpertsGb = cachePlan.cacheGb;
|
|
392
408
|
const kvDir = join(home, 'engines', 'ds4', 'kv');
|
|
@@ -433,6 +449,11 @@ export async function buildDs4Provider(opts) {
|
|
|
433
449
|
? 'interactive'
|
|
434
450
|
: 'background',
|
|
435
451
|
requirement: async () => {
|
|
452
|
+
// A measured launch is reserved at exactly its working set: no
|
|
453
|
+
// full-residency floor, and the expert cache this launch was given.
|
|
454
|
+
const measured = measuredLaunchWith(dspark.enabled ? dsparkCompanionBytes : 0);
|
|
455
|
+
if (measured)
|
|
456
|
+
return { bytes: measured.workingSetBytes, exclusive: !ssdStreaming };
|
|
436
457
|
const { ds4AdmissionGpuBytes, ds4BaseResidentBytes, ds4ResidentLine, ds4ProjectedResidentBytes, ds4ResidentBytesForMode, } = await import('./residency.js');
|
|
437
458
|
const line = ds4ResidentLine(ds4Source ?? {});
|
|
438
459
|
const projected = line ? ds4ProjectedResidentBytes(line, numCtx) : ds4Source?.residentBytes;
|
|
@@ -444,7 +465,7 @@ export async function buildDs4Provider(opts) {
|
|
|
444
465
|
modelSizeBytes: residentWeightBytes,
|
|
445
466
|
});
|
|
446
467
|
const workingSetBytes = base + visionResidentBytes + (dspark.enabled ? dsparkCompanionBytes : 0);
|
|
447
|
-
const bytes = ds4ResidentBytesForMode(workingSetBytes, ssdStreaming,
|
|
468
|
+
const bytes = ds4ResidentBytesForMode(workingSetBytes, ssdStreaming, streamingSupported);
|
|
448
469
|
const gpuBytes = ds4AdmissionGpuBytes({
|
|
449
470
|
workingSetBytes,
|
|
450
471
|
reservationBytes: bytes,
|
|
@@ -528,6 +549,7 @@ export async function buildDs4Provider(opts) {
|
|
|
528
549
|
`backend=${backendFlag.slice(2)} ctx=${numCtx} ` +
|
|
529
550
|
`prefillChunk=${ds4Source?.prefillChunk ?? 'engine-default'} ` +
|
|
530
551
|
`ssdStreaming=${ssdStreaming} cacheExpertsGiB=${ssdStreaming ? cacheExpertsGb : 0} ` +
|
|
552
|
+
`memory=${measuredLaunchWith(0) ? 'measured' : 'catalog'} ` +
|
|
531
553
|
`vision=${vision.enabled} mtp=${mtp.enabled} exactSampling=${mtp.exactSampling} (${mtp.reason}) ` +
|
|
532
554
|
`dspark=${dspark.enabled} (${dspark.reason})`);
|
|
533
555
|
return { command: binary, args, baseUrl: `http://127.0.0.1:${port}`, cwd: ds4BundleDir };
|