ltcai 12.2.0 → 12.3.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.
- package/README.md +92 -84
- package/docs/CHANGELOG.md +64 -4
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +11 -11
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/ROADMAP.md +8 -8
- package/docs/TRUST_MODEL.md +2 -2
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/embedding_providers/autodetect.py +0 -1
- package/latticeai/core/model_compat.py +2 -2
- package/latticeai/core/vector_index/hnsw.py +49 -10
- package/latticeai/core/vector_index/sidecar.py +120 -31
- package/latticeai/models/model_providers.py +10 -11
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/model_capability_registry.py +2 -2
- package/latticeai/services/model_catalog.py +20 -12
- package/latticeai/services/model_engines.py +26 -14
- package/latticeai/services/model_runtime/download.py +53 -6
- package/latticeai/services/product_readiness.py +2 -2
- package/package.json +4 -6
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/release_screen_claims.json +24 -0
- package/src-tauri/Cargo.lock +10 -10
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +44 -44
- package/static/app/assets/{Act-BjGgeuQd.js → Act-CZXyMcfJ.js} +2 -2
- package/static/app/assets/{AdminConsole-C6hcJGMr.js → AdminConsole-CAkZk0BI.js} +1 -1
- package/static/app/assets/{Brain-CNQnIaA8.js → Brain-DN_7_HSV.js} +2 -2
- package/static/app/assets/{BrainHome-CGJ6O69l.js → BrainHome-BBP0ouU-.js} +1 -1
- package/static/app/assets/{BrainSignals-DONPljxz.js → BrainSignals-nyRNxvQ3.js} +1 -1
- package/static/app/assets/{Capture-BMCC5544.js → Capture-DBvFoZKL.js} +1 -1
- package/static/app/assets/{Chronicle-DF6sOonx.js → Chronicle-BC5zmR44.js} +1 -1
- package/static/app/assets/{CommandPalette-DiWyutdM.js → CommandPalette-Bs-Fk6HT.js} +1 -1
- package/static/app/assets/{DigitalBrainExplorer-BL3V6Dqz.js → DigitalBrainExplorer-CNcCktPP.js} +1 -1
- package/static/app/assets/{Library-B-ue9hXL.js → Library-CxUA4xai.js} +1 -1
- package/static/app/assets/{LivingBrain-b1RiM5Rh.js → LivingBrain-CiyjvwKy.js} +1 -1
- package/static/app/assets/ProductFlow-DccERGr3.js +1 -0
- package/static/app/assets/{ReviewCard-D1fGTEn7.js → ReviewCard-lawM1khV.js} +1 -1
- package/static/app/assets/{RunsListPanel-DAirtLkS.js → RunsListPanel-DQC-zQ7m.js} +1 -1
- package/static/app/assets/{System-BEI1eoC4.js → System-DwqYJQaj.js} +1 -1
- package/static/app/assets/{WorkflowGraph-qrI6GU4R.js → WorkflowGraph-pJ2eFrhy.js} +1 -1
- package/static/app/assets/{WorkflowsPanel-CIMNTw-O.js → WorkflowsPanel-BIGjfM7h.js} +2 -2
- package/static/app/assets/{actHelpers-BMukIRnQ.js → actHelpers-DfIqS92u.js} +1 -1
- package/static/app/assets/arrow-left-DvwQxqcD.js +1 -0
- package/static/app/assets/{bot-Dkd-DbiP.js → bot-CAfN5QD1.js} +1 -1
- package/static/app/assets/{brain-BsexlNOA.js → brain-DNNLFQgq.js} +1 -1
- package/static/app/assets/{button-BA0N_zId.js → button-D0k7_k6e.js} +1 -1
- package/static/app/assets/circle-check-hH6updC-.js +1 -0
- package/static/app/assets/{circle-pause-918Kuvk0.js → circle-pause-Dxw6SJuA.js} +1 -1
- package/static/app/assets/{circle-play-CS0IbVDV.js → circle-play-DuoLDLEg.js} +1 -1
- package/static/app/assets/{cpu-BpvoWsjk.js → cpu-BGOpEdGX.js} +1 -1
- package/static/app/assets/{download-BvkMLLLN.js → download-wXD9L5vN.js} +1 -1
- package/static/app/assets/{folder-open-YmGugIBf.js → folder-open-CittV8Ey.js} +1 -1
- package/static/app/assets/{hard-drive-BOzmS-cq.js → hard-drive-CLQPYYBF.js} +1 -1
- package/static/app/assets/{index-CfeqZ9Ri.js → index-C0VEkbAp.js} +3 -3
- package/static/app/assets/{input-CFfCfMjO.js → input-RZ8aKj6d.js} +1 -1
- package/static/app/assets/{link-2-CcYzeM7M.js → link-2-B7QYeNdJ.js} +1 -1
- package/static/app/assets/{permissionCopy-B23r24oX.js → permissionCopy-DFGEuU2d.js} +1 -1
- package/static/app/assets/{primitives-BDryQl6H.js → primitives-EKxh12KR.js} +1 -1
- package/static/app/assets/search-BA5Ge197.js +1 -0
- package/static/app/assets/{share-2-B-gPv2ec.js → share-2-Drg0MPgx.js} +1 -1
- package/static/app/assets/{shield-alert-CfyRO1p0.js → shield-alert-CFKdCGM8.js} +1 -1
- package/static/app/assets/{sourceMeta-C4LDEy1S.js → sourceMeta-BzqJiEPI.js} +1 -1
- package/static/app/assets/{textarea-BdVMYEb1.js → textarea-n_69mQP_.js} +1 -1
- package/static/app/assets/{useFocusTrap-CV73DLHn.js → useFocusTrap-B0w4AxK1.js} +1 -1
- package/static/app/assets/{useMutation-B8QKq2NW.js → useMutation-Dvcq6J6b.js} +1 -1
- package/static/app/assets/workspace-BgBHikYl.js +1 -0
- package/static/app/index.html +1 -1
- package/static/sw.js +1 -1
- package/static/app/assets/ProductFlow-wZIKJW7G.js +0 -1
- package/static/app/assets/arrow-left-iVa1goff.js +0 -1
- package/static/app/assets/circle-check-BxaDWjPR.js +0 -1
- package/static/app/assets/search-DHsPwmPw.js +0 -1
- package/static/app/assets/workspace-BmfVs66W.js +0 -1
|
@@ -118,6 +118,11 @@ class HnswIndex:
|
|
|
118
118
|
def model_id(self) -> str:
|
|
119
119
|
return self._model_id
|
|
120
120
|
|
|
121
|
+
@property
|
|
122
|
+
def labels(self) -> List[str]:
|
|
123
|
+
"""Live label order — the sidecar's identity, loaded or rebuilt."""
|
|
124
|
+
return list(self._labels)
|
|
125
|
+
|
|
121
126
|
def _identity_matches(self, dim: int, space: str, model_id: str) -> bool:
|
|
122
127
|
return (
|
|
123
128
|
int(dim) == self._dim
|
|
@@ -177,10 +182,12 @@ class HnswIndex:
|
|
|
177
182
|
doors without guessing.
|
|
178
183
|
|
|
179
184
|
Full rebuild when the embedder identity changed, when any currently
|
|
180
|
-
indexed id is missing from
|
|
181
|
-
deletes would exceed :data:`DELETE_REBUILD_RATIO
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
indexed id is missing from a *full* incoming snapshot (a deletion),
|
|
186
|
+
or when deletes would exceed :data:`DELETE_REBUILD_RATIO`. A sidecar
|
|
187
|
+
that was loaded without source vectors still **appends**: the
|
|
188
|
+
caller passes only the new ids (COUNT+delta) and ``_append_live``
|
|
189
|
+
extends the graph. A disjoint incoming set is that delta, not a
|
|
190
|
+
wipe.
|
|
184
191
|
"""
|
|
185
192
|
incoming: Dict[str, IndexItem] = {}
|
|
186
193
|
for item_id, vector, metadata in items:
|
|
@@ -197,13 +204,36 @@ class HnswIndex:
|
|
|
197
204
|
return "rebuild"
|
|
198
205
|
|
|
199
206
|
if self._loaded:
|
|
207
|
+
current = set(self._labels)
|
|
208
|
+
incoming_ids = set(incoming)
|
|
209
|
+
overlap = current & incoming_ids
|
|
210
|
+
deleted = current - incoming_ids
|
|
211
|
+
# Delta-only: the caller passed new ids, none of the live labels.
|
|
212
|
+
# A full snapshot that dropped ids is a deletion and must rebuild.
|
|
213
|
+
delta_only = bool(incoming_ids) and not overlap
|
|
214
|
+
if deleted and not delta_only:
|
|
215
|
+
self.rebuild(incoming.values())
|
|
216
|
+
return "rebuild"
|
|
217
|
+
new_ids = [item_id for item_id in incoming if item_id not in current]
|
|
218
|
+
if not new_ids:
|
|
219
|
+
return "append"
|
|
220
|
+
for item_id in new_ids:
|
|
221
|
+
_, vector, metadata = incoming[item_id]
|
|
222
|
+
self._vectors[item_id] = [float(value) for value in vector]
|
|
223
|
+
self._metadata[item_id] = dict(metadata or {})
|
|
224
|
+
if self._ann is not None and self._module is not None:
|
|
225
|
+
self._append_live(new_ids)
|
|
226
|
+
return "append"
|
|
200
227
|
self.rebuild(incoming.values())
|
|
201
228
|
return "rebuild"
|
|
202
229
|
|
|
203
230
|
current = set(self._vectors)
|
|
204
231
|
incoming_ids = set(incoming)
|
|
205
232
|
deleted = current - incoming_ids
|
|
206
|
-
|
|
233
|
+
overlap = current & incoming_ids
|
|
234
|
+
# Same discriminant as the loaded path: a disjoint incoming set is
|
|
235
|
+
# COUNT+delta, not a snapshot that deleted every live id.
|
|
236
|
+
if deleted and not (bool(incoming_ids) and not overlap):
|
|
207
237
|
ratio = len(deleted) / max(1, len(current))
|
|
208
238
|
if ratio >= DELETE_REBUILD_RATIO or not current:
|
|
209
239
|
self.rebuild(incoming.values())
|
|
@@ -339,18 +369,25 @@ class HnswIndex:
|
|
|
339
369
|
return False
|
|
340
370
|
return True
|
|
341
371
|
|
|
342
|
-
def load(self, db_path: Any, *, fingerprint: str) -> bool:
|
|
343
|
-
"""Adopt a sidecar graph when it
|
|
372
|
+
def load(self, db_path: Any, *, fingerprint: str, strict: bool = True) -> bool:
|
|
373
|
+
"""Adopt a sidecar graph when it matches this embedder identity.
|
|
374
|
+
|
|
375
|
+
``strict`` (the default) also requires the stored fingerprint — size
|
|
376
|
+
included — so a grown store does not search a stale graph. Relaxed
|
|
377
|
+
load keeps dim/model and leaves COUNT+delta to append the missing
|
|
378
|
+
ids rather than dumping the whole table.
|
|
379
|
+
"""
|
|
344
380
|
module = self._module
|
|
345
381
|
if module is None:
|
|
346
382
|
return False
|
|
347
383
|
index_path, meta_path = sidecar_paths(db_path)
|
|
348
384
|
try:
|
|
349
385
|
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
350
|
-
matches =
|
|
351
|
-
matches = matches and int(meta["dim"]) == self._dim
|
|
386
|
+
matches = int(meta["dim"]) == self._dim
|
|
352
387
|
if meta.get("model_id") not in (None, "", self._model_id):
|
|
353
388
|
matches = False
|
|
389
|
+
if strict:
|
|
390
|
+
matches = matches and str(meta["fingerprint"]) == str(fingerprint)
|
|
354
391
|
labels = [str(label) for label in meta["labels"]]
|
|
355
392
|
except Exception: # noqa: BLE001 — absent/corrupt sidecar = rebuild
|
|
356
393
|
return False
|
|
@@ -358,7 +395,9 @@ class HnswIndex:
|
|
|
358
395
|
return False
|
|
359
396
|
try:
|
|
360
397
|
graph = module.Index(space=self._space, dim=self._dim)
|
|
361
|
-
|
|
398
|
+
# Headroom so an in-process append does not have to rebuild
|
|
399
|
+
# just because load_index pinned max_elements to the old size.
|
|
400
|
+
graph.load_index(str(index_path), max_elements=max(len(labels) + 64, 1))
|
|
362
401
|
except Exception: # noqa: BLE001 — corrupt binary = rebuild
|
|
363
402
|
return False
|
|
364
403
|
self._ann = graph
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
The live graph is cached per ``(db_path, model_id, dim)``. A missing sidecar
|
|
4
4
|
is ``index: "none"``. When the store has vectors the cache (or a rebuild from
|
|
5
|
-
``vector_embeddings``) can serve, the reply is ``index: "hnsw"``.
|
|
6
|
-
sidecar size vs ``COUNT(*)`` for that identity
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
``vector_embeddings``) can serve, the reply is ``index: "hnsw"``. Freshness is
|
|
6
|
+
sidecar size vs ``COUNT(*)`` for that identity. A warm COUNT match never
|
|
7
|
+
opens embedding blobs. Growth loads **missing ids only** and appends; a
|
|
8
|
+
shrink still rebuilds from the remaining rows.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -81,6 +81,83 @@ def load_store_items(
|
|
|
81
81
|
return items
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def load_store_ids(conn: sqlite3.Connection, model_id: str, dim: int) -> List[str]:
|
|
85
|
+
"""Item ids only — the COUNT+delta path must not pull embedding blobs."""
|
|
86
|
+
rows = conn.execute(
|
|
87
|
+
"SELECT item_id FROM vector_embeddings "
|
|
88
|
+
"WHERE embedding_model=? AND embedding_dim=? ORDER BY indexed_at ASC, item_id ASC",
|
|
89
|
+
(model_id, int(dim)),
|
|
90
|
+
)
|
|
91
|
+
return [str(item_id) for (item_id,) in rows]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_store_items_for(
|
|
95
|
+
conn: sqlite3.Connection,
|
|
96
|
+
model_id: str,
|
|
97
|
+
dim: int,
|
|
98
|
+
item_ids: Sequence[str],
|
|
99
|
+
) -> List[Tuple[str, List[float], Dict[str, Any]]]:
|
|
100
|
+
"""Blobs for ``item_ids`` only, preserving the caller's order."""
|
|
101
|
+
if not item_ids:
|
|
102
|
+
return []
|
|
103
|
+
by_id: Dict[str, List[float]] = {}
|
|
104
|
+
wanted = [str(item_id) for item_id in item_ids]
|
|
105
|
+
# SQLite caps bound parameters; 400 leaves room for the identity pair.
|
|
106
|
+
for start in range(0, len(wanted), 400):
|
|
107
|
+
chunk = wanted[start : start + 400]
|
|
108
|
+
qmarks = ",".join("?" * len(chunk))
|
|
109
|
+
query = "SELECT item_id, embedding FROM vector_embeddings WHERE embedding_model=? AND embedding_dim=? AND item_id IN (%s)" % qmarks # noqa: S608
|
|
110
|
+
for item_id, blob in conn.execute(query, (model_id, int(dim), *chunk)):
|
|
111
|
+
vector = decode_f32le(bytes(blob or b""), int(dim))
|
|
112
|
+
if len(vector) != int(dim):
|
|
113
|
+
continue
|
|
114
|
+
by_id[str(item_id)] = vector
|
|
115
|
+
return [
|
|
116
|
+
(item_id, by_id[item_id], {})
|
|
117
|
+
for item_id in wanted
|
|
118
|
+
if item_id in by_id
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _open_and_count(path: Path, model_id: str, dim: int) -> int:
|
|
123
|
+
conn = _open_readonly(path)
|
|
124
|
+
if conn is None:
|
|
125
|
+
return 0
|
|
126
|
+
try:
|
|
127
|
+
return store_vector_count(conn, model_id, dim)
|
|
128
|
+
finally:
|
|
129
|
+
conn.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _load_all_items(
|
|
133
|
+
path: Path, model_id: str, dim: int
|
|
134
|
+
) -> List[Tuple[str, List[float], Dict[str, Any]]]:
|
|
135
|
+
conn = _open_readonly(path)
|
|
136
|
+
if conn is None:
|
|
137
|
+
return []
|
|
138
|
+
try:
|
|
139
|
+
return load_store_items(conn, model_id, dim)
|
|
140
|
+
finally:
|
|
141
|
+
conn.close()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _load_missing_items(
|
|
145
|
+
path: Path,
|
|
146
|
+
model_id: str,
|
|
147
|
+
dim: int,
|
|
148
|
+
have: Sequence[str],
|
|
149
|
+
) -> List[Tuple[str, List[float], Dict[str, Any]]]:
|
|
150
|
+
conn = _open_readonly(path)
|
|
151
|
+
if conn is None:
|
|
152
|
+
return []
|
|
153
|
+
try:
|
|
154
|
+
have_set = set(have)
|
|
155
|
+
missing = [item_id for item_id in load_store_ids(conn, model_id, dim) if item_id not in have_set]
|
|
156
|
+
return load_store_items_for(conn, model_id, dim, missing)
|
|
157
|
+
finally:
|
|
158
|
+
conn.close()
|
|
159
|
+
|
|
160
|
+
|
|
84
161
|
def sidecar_meta_size(db_path: Path) -> Optional[int]:
|
|
85
162
|
"""Label count from the sidecar meta file, or ``None`` when it is absent."""
|
|
86
163
|
_, meta_path = sidecar_paths(db_path)
|
|
@@ -185,17 +262,8 @@ def query_sidecar(
|
|
|
185
262
|
if not model_id:
|
|
186
263
|
return _none_reply(detail="embedding_model is required")
|
|
187
264
|
|
|
188
|
-
|
|
189
|
-
store_size =
|
|
190
|
-
items: List[Tuple[str, List[float], Dict[str, Any]]] = []
|
|
191
|
-
if conn is not None:
|
|
192
|
-
try:
|
|
193
|
-
store_size = store_vector_count(conn, model_id, dim)
|
|
194
|
-
if store_size > 0:
|
|
195
|
-
items = load_store_items(conn, model_id, dim)
|
|
196
|
-
store_size = len(items)
|
|
197
|
-
finally:
|
|
198
|
-
conn.close()
|
|
265
|
+
# COUNT first. Warm queries that already match never open embedding blobs.
|
|
266
|
+
store_size = _open_and_count(path, model_id, dim)
|
|
199
267
|
|
|
200
268
|
key = (str(path), model_id, dim)
|
|
201
269
|
with _LOCK:
|
|
@@ -203,8 +271,20 @@ def query_sidecar(
|
|
|
203
271
|
refreshed: Optional[str] = None
|
|
204
272
|
if index is None:
|
|
205
273
|
index = HnswIndex(dim=dim, model_id=model_id)
|
|
206
|
-
loaded = index.load(
|
|
274
|
+
loaded = index.load(
|
|
275
|
+
path, fingerprint=sidecar_fingerprint(model_id, dim, store_size)
|
|
276
|
+
)
|
|
277
|
+
if not loaded and store_size > 0:
|
|
278
|
+
# Sidecar exists for this identity but the COUNT moved —
|
|
279
|
+
# load the graph and append the missing ids below.
|
|
280
|
+
loaded = index.load(
|
|
281
|
+
path,
|
|
282
|
+
fingerprint=sidecar_fingerprint(model_id, dim, store_size),
|
|
283
|
+
strict=False,
|
|
284
|
+
)
|
|
207
285
|
if not loaded:
|
|
286
|
+
items = _load_all_items(path, model_id, dim)
|
|
287
|
+
store_size = len(items)
|
|
208
288
|
if not items:
|
|
209
289
|
return _none_reply(
|
|
210
290
|
store_size=store_size,
|
|
@@ -216,25 +296,31 @@ def query_sidecar(
|
|
|
216
296
|
|
|
217
297
|
cached_size = int(index.stats().get("size") or 0)
|
|
218
298
|
stale = cached_size != store_size
|
|
219
|
-
if stale:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
299
|
+
if stale and store_size == 0:
|
|
300
|
+
_CACHE.pop(key, None)
|
|
301
|
+
return _none_reply(
|
|
302
|
+
store_size=0,
|
|
303
|
+
detail="vector store is empty for this embedding identity",
|
|
304
|
+
)
|
|
305
|
+
if stale and store_size > cached_size:
|
|
306
|
+
delta = _load_missing_items(path, model_id, dim, index.labels)
|
|
307
|
+
if delta:
|
|
308
|
+
refreshed = _refresh_index(index, delta, model_id, dim)
|
|
309
|
+
cached_size = int(index.stats().get("size") or 0)
|
|
310
|
+
index.save(
|
|
311
|
+
path, fingerprint=sidecar_fingerprint(model_id, dim, cached_size)
|
|
312
|
+
)
|
|
313
|
+
stale = cached_size != store_size
|
|
314
|
+
elif stale:
|
|
315
|
+
items = _load_all_items(path, model_id, dim)
|
|
316
|
+
store_size = len(items)
|
|
227
317
|
if items:
|
|
228
318
|
refreshed = _refresh_index(index, items, model_id, dim)
|
|
229
|
-
index.save(path, fingerprint=sidecar_fingerprint(model_id, dim, len(items)))
|
|
230
319
|
cached_size = int(index.stats().get("size") or 0)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
_CACHE.pop(key, None)
|
|
234
|
-
return _none_reply(
|
|
235
|
-
store_size=0,
|
|
236
|
-
detail="vector store is empty for this embedding identity",
|
|
320
|
+
index.save(
|
|
321
|
+
path, fingerprint=sidecar_fingerprint(model_id, dim, cached_size)
|
|
237
322
|
)
|
|
323
|
+
stale = cached_size != store_size
|
|
238
324
|
|
|
239
325
|
if cached_size <= 0:
|
|
240
326
|
return _none_reply(
|
|
@@ -326,4 +412,7 @@ __all__ = [
|
|
|
326
412
|
"sidecar_freshness",
|
|
327
413
|
"sidecar_meta_size",
|
|
328
414
|
"store_vector_count",
|
|
415
|
+
"load_store_ids",
|
|
416
|
+
"load_store_items",
|
|
417
|
+
"load_store_items_for",
|
|
329
418
|
]
|
|
@@ -27,29 +27,28 @@ OPENAI_COMPATIBLE_PROVIDERS = {
|
|
|
27
27
|
"together": {
|
|
28
28
|
"env_key": "TOGETHER_API_KEY",
|
|
29
29
|
"base_url": "https://api.together.xyz/v1",
|
|
30
|
-
"default_model": "Qwen/Qwen3-
|
|
30
|
+
"default_model": "Qwen/Qwen3.6-27B",
|
|
31
31
|
},
|
|
32
32
|
"xai": {
|
|
33
33
|
"env_key": "XAI_API_KEY",
|
|
34
34
|
"base_url": "https://api.x.ai/v1",
|
|
35
35
|
# ``grok-beta`` was xAI's 2024 preview id and has been decommissioned;
|
|
36
|
-
# a request naming it now 404s at the provider. ``grok-4.
|
|
37
|
-
# current flagship on api.x.ai and
|
|
38
|
-
|
|
39
|
-
"default_model": "grok-4.5",
|
|
36
|
+
# a request naming it now 404s at the provider. ``grok-4.6`` is the
|
|
37
|
+
# current flagship on api.x.ai and matches the Rust cloud adapter.
|
|
38
|
+
"default_model": "grok-4.6",
|
|
40
39
|
},
|
|
41
40
|
"ollama": {
|
|
42
41
|
"env_key": "OLLAMA_API_KEY",
|
|
43
42
|
"base_url_env": "OLLAMA_BASE_URL",
|
|
44
43
|
"base_url": "http://localhost:11434/v1",
|
|
45
|
-
"default_model": "hf.co/ggml-org/gemma-4-12B-it-GGUF:
|
|
44
|
+
"default_model": "hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_0",
|
|
46
45
|
"api_key_fallback": "ollama",
|
|
47
46
|
},
|
|
48
47
|
"vllm": {
|
|
49
48
|
"env_key": "VLLM_API_KEY",
|
|
50
49
|
"base_url_env": "VLLM_BASE_URL",
|
|
51
50
|
"base_url": "http://localhost:8000/v1",
|
|
52
|
-
"default_model": "
|
|
51
|
+
"default_model": "google/gemma-4-12B-it",
|
|
53
52
|
"api_key_fallback": "vllm",
|
|
54
53
|
},
|
|
55
54
|
"lmstudio": {
|
|
@@ -85,9 +84,9 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
85
84
|
{"id": "anthropic/claude-opus-4.7", "name": "Claude Opus 4.7 via OpenRouter", "family": "Claude"},
|
|
86
85
|
{"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6 via OpenRouter", "family": "Claude"},
|
|
87
86
|
{"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
|
|
88
|
-
{"id": "qwen/qwen3-
|
|
87
|
+
{"id": "qwen/qwen3.6-35b-a3b", "name": "Qwen3.6 35B A3B via OpenRouter", "family": "Qwen"},
|
|
89
88
|
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
|
|
90
|
-
{"id": "x-ai/grok-4.
|
|
89
|
+
{"id": "x-ai/grok-4.6", "name": "Grok 4.6 via OpenRouter", "family": "Grok"},
|
|
91
90
|
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
|
|
92
91
|
{"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
|
|
93
92
|
],
|
|
@@ -95,7 +94,7 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
95
94
|
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
96
95
|
],
|
|
97
96
|
"together": [
|
|
98
|
-
{"id": "Qwen/Qwen3-
|
|
97
|
+
{"id": "Qwen/Qwen3.6-27B", "name": "Qwen3.6 27B", "family": "Qwen"},
|
|
99
98
|
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B", "family": "Gemma"},
|
|
100
99
|
{"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
101
100
|
],
|
|
@@ -105,7 +104,7 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
105
104
|
# (grok-beta / grok-vision-beta) are not carried as fallbacks: a model
|
|
106
105
|
# the provider no longer serves is not compatibility, it is a 404 the
|
|
107
106
|
# user has to discover for themselves.
|
|
108
|
-
{"id": "grok-4.
|
|
107
|
+
{"id": "grok-4.6", "name": "Grok 4.6", "family": "Grok"},
|
|
109
108
|
],
|
|
110
109
|
}
|
|
111
110
|
|
|
@@ -300,7 +300,7 @@ _REGISTRY: List[ModelCapability] = [
|
|
|
300
300
|
architecture="qwen3_5",
|
|
301
301
|
download_size_gb=5.98,
|
|
302
302
|
quantization="4bit",
|
|
303
|
-
provider_hints=["local_mlx", "vllm"],
|
|
303
|
+
provider_hints=["local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"],
|
|
304
304
|
download_strategy="hf_hub",
|
|
305
305
|
load_strategy="mlx_vlm",
|
|
306
306
|
hardware=HardwareProfile(
|
|
@@ -381,7 +381,7 @@ _REGISTRY: List[ModelCapability] = [
|
|
|
381
381
|
architecture="qwen3_5_moe",
|
|
382
382
|
download_size_gb=20.43,
|
|
383
383
|
quantization="4bit",
|
|
384
|
-
provider_hints=["local_mlx", "vllm"],
|
|
384
|
+
provider_hints=["local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"],
|
|
385
385
|
download_strategy="hf_hub",
|
|
386
386
|
load_strategy="mlx_vlm",
|
|
387
387
|
hardware=HardwareProfile(
|
|
@@ -102,11 +102,17 @@ ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, Any]]] = {}
|
|
|
102
102
|
# gated — the download could never succeed without Hub credentials.
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def _gguf_routes(
|
|
105
|
+
def _gguf_routes(
|
|
106
|
+
mlx_repo: str,
|
|
107
|
+
gguf_repo: str,
|
|
108
|
+
hf_repo: str,
|
|
109
|
+
*,
|
|
110
|
+
ollama_tag: str = "Q4_K_M",
|
|
111
|
+
) -> Dict[str, str]:
|
|
106
112
|
"""One model's engine map: MLX weights, a GGUF repo, and the upstream HF repo."""
|
|
107
113
|
return {
|
|
108
114
|
"local_mlx": mlx_repo,
|
|
109
|
-
"ollama": f"hf.co/{gguf_repo}:
|
|
115
|
+
"ollama": f"hf.co/{gguf_repo}:{ollama_tag}",
|
|
110
116
|
"vllm": hf_repo,
|
|
111
117
|
"lmstudio": gguf_repo,
|
|
112
118
|
"llamacpp": gguf_repo,
|
|
@@ -119,36 +125,38 @@ _ENGINE_ROUTES: Dict[str, Dict[str, str]] = {
|
|
|
119
125
|
),
|
|
120
126
|
"mlx-community/gemma-4-e2b-it-4bit": _gguf_routes(
|
|
121
127
|
"mlx-community/gemma-4-e2b-it-4bit", "ggml-org/gemma-4-E2B-it-GGUF", "google/gemma-4-E2B-it",
|
|
128
|
+
ollama_tag="Q4_0",
|
|
122
129
|
),
|
|
123
130
|
"mlx-community/gemma-4-e4b-it-4bit": _gguf_routes(
|
|
124
131
|
"mlx-community/gemma-4-e4b-it-4bit", "ggml-org/gemma-4-E4B-it-GGUF", "google/gemma-4-E4B-it",
|
|
132
|
+
ollama_tag="Q4_0",
|
|
125
133
|
),
|
|
126
134
|
"mlx-community/gemma-4-12B-it-4bit": _gguf_routes(
|
|
127
135
|
"mlx-community/gemma-4-12B-it-4bit", "ggml-org/gemma-4-12B-it-GGUF", "google/gemma-4-12B-it",
|
|
136
|
+
ollama_tag="Q4_0",
|
|
128
137
|
),
|
|
129
138
|
"mlx-community/gemma-4-26b-a4b-it-4bit": _gguf_routes(
|
|
130
139
|
"mlx-community/gemma-4-26b-a4b-it-4bit", "ggml-org/gemma-4-26B-A4B-it-GGUF",
|
|
131
140
|
"google/gemma-4-26B-A4B-it",
|
|
141
|
+
ollama_tag="Q4_0",
|
|
132
142
|
),
|
|
133
143
|
"mlx-community/gemma-4-31b-it-4bit": _gguf_routes(
|
|
134
144
|
"mlx-community/gemma-4-31b-it-4bit", "ggml-org/gemma-4-31B-it-GGUF", "google/gemma-4-31B-it",
|
|
145
|
+
ollama_tag="Q4_0",
|
|
135
146
|
),
|
|
136
147
|
"mlx-community/Qwen3.6-27B-4bit": _gguf_routes(
|
|
137
148
|
"mlx-community/Qwen3.6-27B-4bit", "ggml-org/Qwen3.6-27B-GGUF", "Qwen/Qwen3.6-27B",
|
|
138
149
|
),
|
|
139
150
|
"mlx-community/gpt-oss-20b-MXFP4-Q8": _gguf_routes(
|
|
140
151
|
"mlx-community/gpt-oss-20b-MXFP4-Q8", "ggml-org/gpt-oss-20b-GGUF", "openai/gpt-oss-20b",
|
|
152
|
+
ollama_tag="MXFP4",
|
|
153
|
+
),
|
|
154
|
+
"mlx-community/Qwen3.5-9B-MLX-4bit": _gguf_routes(
|
|
155
|
+
"mlx-community/Qwen3.5-9B-MLX-4bit", "unsloth/Qwen3.5-9B-GGUF", "Qwen/Qwen3.5-9B",
|
|
156
|
+
),
|
|
157
|
+
"mlx-community/Qwen3.6-35B-A3B-4bit": _gguf_routes(
|
|
158
|
+
"mlx-community/Qwen3.6-35B-A3B-4bit", "ggml-org/Qwen3.6-35B-A3B-GGUF", "Qwen/Qwen3.6-35B-A3B",
|
|
141
159
|
),
|
|
142
|
-
# No community GGUF build verified for these two, so they offer the MLX
|
|
143
|
-
# weights and the upstream repo only rather than a route that would 404.
|
|
144
|
-
"mlx-community/Qwen3.5-9B-MLX-4bit": {
|
|
145
|
-
"local_mlx": "mlx-community/Qwen3.5-9B-MLX-4bit",
|
|
146
|
-
"vllm": "Qwen/Qwen3.5-9B",
|
|
147
|
-
},
|
|
148
|
-
"mlx-community/Qwen3.6-35B-A3B-4bit": {
|
|
149
|
-
"local_mlx": "mlx-community/Qwen3.6-35B-A3B-4bit",
|
|
150
|
-
"vllm": "Qwen/Qwen3.6-35B-A3B",
|
|
151
|
-
},
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
# Names people actually type that are not the repo's own short name. Kept
|
|
@@ -332,6 +332,10 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
332
332
|
hf_model_dir,
|
|
333
333
|
hf_model_ready,
|
|
334
334
|
)
|
|
335
|
+
from latticeai.services.model_runtime.download import (
|
|
336
|
+
GGUF_QUANT_PREFERENCE,
|
|
337
|
+
is_auxiliary_gguf,
|
|
338
|
+
)
|
|
335
339
|
|
|
336
340
|
served_models = get_openai_compatible_server_models("llamacpp")
|
|
337
341
|
if model_name in served_models:
|
|
@@ -347,23 +351,31 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
347
351
|
download_hf_model(model_name, "llamacpp")
|
|
348
352
|
|
|
349
353
|
gguf_files = sorted(hf_model_dir(model_name).rglob("*.gguf"))
|
|
350
|
-
if not
|
|
354
|
+
primary_ggufs = [p for p in gguf_files if not is_auxiliary_gguf(p.name)]
|
|
355
|
+
if not primary_ggufs:
|
|
351
356
|
raise ModelRuntimeError(status_code=500, detail="다운로드된 GGUF 파일을 찾지 못했습니다.")
|
|
352
357
|
|
|
353
|
-
preferred = next(
|
|
354
|
-
|
|
358
|
+
preferred = next(
|
|
359
|
+
(p for pref in GGUF_QUANT_PREFERENCE for p in primary_ggufs if pref in p.name.lower()),
|
|
360
|
+
None,
|
|
361
|
+
)
|
|
362
|
+
model_file = preferred or primary_ggufs[0]
|
|
363
|
+
command = [
|
|
364
|
+
"llama-server",
|
|
365
|
+
"-m",
|
|
366
|
+
str(model_file),
|
|
367
|
+
"--alias",
|
|
368
|
+
model_name,
|
|
369
|
+
"--host",
|
|
370
|
+
"127.0.0.1",
|
|
371
|
+
"--port",
|
|
372
|
+
"8080",
|
|
373
|
+
]
|
|
374
|
+
mmprojs = [p for p in gguf_files if p.name.lower().startswith("mmproj-")]
|
|
375
|
+
if mmprojs:
|
|
376
|
+
command.extend(["--mmproj", str(mmprojs[0])])
|
|
355
377
|
LOCAL_SERVER_PROCESSES["llamacpp"] = subprocess.Popen(
|
|
356
|
-
|
|
357
|
-
"llama-server",
|
|
358
|
-
"-m",
|
|
359
|
-
str(model_file),
|
|
360
|
-
"--alias",
|
|
361
|
-
model_name,
|
|
362
|
-
"--host",
|
|
363
|
-
"127.0.0.1",
|
|
364
|
-
"--port",
|
|
365
|
-
"8080",
|
|
366
|
-
],
|
|
378
|
+
command,
|
|
367
379
|
stdout=subprocess.DEVNULL,
|
|
368
380
|
stderr=subprocess.DEVNULL,
|
|
369
381
|
start_new_session=True,
|
|
@@ -10,6 +10,7 @@ differently.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import importlib.util
|
|
13
|
+
import json
|
|
13
14
|
import logging
|
|
14
15
|
import time
|
|
15
16
|
from pathlib import Path
|
|
@@ -19,6 +20,29 @@ from latticeai.core.quiet import quiet
|
|
|
19
20
|
from latticeai.models.router import hf_cache_model_dir, hf_model_dir
|
|
20
21
|
from latticeai.services.model_errors import ModelRuntimeError
|
|
21
22
|
|
|
23
|
+
_AUXILIARY_GGUF_PREFIXES = ("mmproj-", "mtp-", "dflash-", "eagle3-")
|
|
24
|
+
GGUF_QUANT_PREFERENCE = ("q4_k_m", "q4_0", "mxfp4", "q4_k_s", "q3_k_m", "q2_k", "q8_0")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_auxiliary_gguf(filename: str) -> bool:
|
|
28
|
+
base = Path(str(filename)).name.lower()
|
|
29
|
+
return base.startswith(_AUXILIARY_GGUF_PREFIXES) or "imatrix" in base
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _shards_complete(model_dir: Path) -> bool:
|
|
33
|
+
index_file = model_dir / "model.safetensors.index.json"
|
|
34
|
+
if not index_file.exists():
|
|
35
|
+
return True
|
|
36
|
+
try:
|
|
37
|
+
payload = json.loads(index_file.read_text(encoding="utf-8"))
|
|
38
|
+
weight_map = payload.get("weight_map") if isinstance(payload, dict) else None
|
|
39
|
+
if isinstance(weight_map, dict) and weight_map:
|
|
40
|
+
shards = {str(shard) for shard in weight_map.values() if shard}
|
|
41
|
+
return all((model_dir / shard).exists() for shard in shards)
|
|
42
|
+
except Exception:
|
|
43
|
+
quiet()
|
|
44
|
+
return True
|
|
45
|
+
|
|
22
46
|
|
|
23
47
|
def hf_model_ready(repo_id: str, provider: str = "local_mlx") -> bool:
|
|
24
48
|
model_dir = hf_model_dir(repo_id)
|
|
@@ -32,9 +56,11 @@ def hf_model_ready(repo_id: str, provider: str = "local_mlx") -> bool:
|
|
|
32
56
|
if not model_dir.exists() or not model_dir.is_dir():
|
|
33
57
|
return False
|
|
34
58
|
if provider == "llamacpp":
|
|
35
|
-
return any(model_dir.rglob("*.gguf"))
|
|
59
|
+
return any(not is_auxiliary_gguf(p.name) for p in model_dir.rglob("*.gguf"))
|
|
36
60
|
has_config = (model_dir / "config.json").exists()
|
|
37
|
-
has_weights =
|
|
61
|
+
has_weights = (
|
|
62
|
+
any(model_dir.glob("*.safetensors")) or any(model_dir.glob("*.bin"))
|
|
63
|
+
) and _shards_complete(model_dir)
|
|
38
64
|
has_tokenizer = (
|
|
39
65
|
(model_dir / "tokenizer.json").exists()
|
|
40
66
|
or (model_dir / "tokenizer.model").exists()
|
|
@@ -139,15 +165,36 @@ def download_hf_model(
|
|
|
139
165
|
[item for item in all_files if str(item["name"]).lower().endswith(".gguf")],
|
|
140
166
|
key=lambda item: str(item["name"]),
|
|
141
167
|
)
|
|
142
|
-
if not
|
|
168
|
+
primary_ggufs = [item for item in ggufs if not is_auxiliary_gguf(str(item["name"]))]
|
|
169
|
+
if not primary_ggufs:
|
|
143
170
|
raise RuntimeError("GGUF 파일을 찾지 못했습니다.")
|
|
144
|
-
preference = ("q4_k_m", "q4_0", "q4_k_s", "q3_k_m", "q2_k")
|
|
145
171
|
selected_files = [
|
|
146
172
|
next(
|
|
147
|
-
(
|
|
148
|
-
|
|
173
|
+
(
|
|
174
|
+
item
|
|
175
|
+
for pref in GGUF_QUANT_PREFERENCE
|
|
176
|
+
for item in primary_ggufs
|
|
177
|
+
if pref in str(item["name"]).lower()
|
|
178
|
+
),
|
|
179
|
+
primary_ggufs[0],
|
|
149
180
|
)
|
|
150
181
|
]
|
|
182
|
+
mmprojs = [
|
|
183
|
+
item
|
|
184
|
+
for item in ggufs
|
|
185
|
+
if Path(str(item["name"])).name.lower().startswith("mmproj-")
|
|
186
|
+
]
|
|
187
|
+
if mmprojs:
|
|
188
|
+
preferred_mmproj = next(
|
|
189
|
+
(
|
|
190
|
+
item
|
|
191
|
+
for pref in ("bf16", "f16", "f32")
|
|
192
|
+
for item in mmprojs
|
|
193
|
+
if pref in str(item["name"]).lower()
|
|
194
|
+
),
|
|
195
|
+
mmprojs[0],
|
|
196
|
+
)
|
|
197
|
+
selected_files.append(preferred_mmproj)
|
|
151
198
|
else:
|
|
152
199
|
selected_files = all_files
|
|
153
200
|
|
|
@@ -28,7 +28,7 @@ from typing import Any, Dict, List
|
|
|
28
28
|
|
|
29
29
|
from latticeai.services.architecture_readiness import architecture_readiness
|
|
30
30
|
|
|
31
|
-
PRODUCT_VERSION_TARGET = "12.
|
|
31
|
+
PRODUCT_VERSION_TARGET = "12.3.0"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@dataclass(frozen=True)
|
|
@@ -123,7 +123,7 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
123
123
|
f"vscode-extension/README.md::**{PRODUCT_VERSION_TARGET}",
|
|
124
124
|
f"docs/CHANGELOG.md::## [{PRODUCT_VERSION_TARGET}]",
|
|
125
125
|
"FEATURE_STATUS.md",
|
|
126
|
-
f"RELEASE_NOTES_v{PRODUCT_VERSION_TARGET}.md",
|
|
126
|
+
f"docs/releases/RELEASE_NOTES_v{PRODUCT_VERSION_TARGET}.md",
|
|
127
127
|
"rust/lattice-agent/src/kernel/permission.rs::pub fn block_reason_for_tool",
|
|
128
128
|
"rust/lattice-agent/src/kernel/mod.rs::pub mod agentloop",
|
|
129
129
|
"latticeai/api/agent_worker_seam.py::create_agent_worker_seam_router",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.3.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -111,16 +111,14 @@
|
|
|
111
111
|
"!TOOLS.md",
|
|
112
112
|
"!USER.md",
|
|
113
113
|
"!docs/images/tmp_frames/",
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
114
|
+
"!{latticeai,lattice_brain,skills,plugins,scripts}/**/__pycache__/",
|
|
115
|
+
"!{latticeai,lattice_brain,skills,plugins,scripts}/**/*.pyc",
|
|
116
|
+
"!static/**/*.map",
|
|
117
117
|
"README.md",
|
|
118
118
|
"static/vendor/",
|
|
119
119
|
"src-tauri/",
|
|
120
120
|
"!src-tauri/gen/",
|
|
121
|
-
"!src-tauri/gen/**",
|
|
122
121
|
"!src-tauri/target/",
|
|
123
|
-
"!src-tauri/target/**",
|
|
124
122
|
"docs/*.md",
|
|
125
123
|
"!docs/assets/",
|
|
126
124
|
"!docs/images/"
|