ltcai 4.4.0 → 4.6.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 +77 -33
- package/docs/CHANGELOG.md +128 -0
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
- package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
- package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
- package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
- package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
- package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
- package/docs/V4_5_1_UX_REPORT.md +45 -0
- package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -17
- package/docs/architecture.md +8 -4
- package/frontend/index.html +2 -2
- package/frontend/src/App.tsx +120 -98
- package/frontend/src/api/client.ts +84 -1
- package/frontend/src/components/BrainConversation.tsx +301 -0
- package/frontend/src/components/FirstRunGuide.tsx +99 -0
- package/frontend/src/components/LivingBrain.tsx +121 -0
- package/frontend/src/components/ProductFlow.tsx +596 -0
- package/frontend/src/components/primitives.tsx +131 -25
- package/frontend/src/components/ui/badge.tsx +2 -2
- package/frontend/src/components/ui/button.tsx +7 -7
- package/frontend/src/components/ui/card.tsx +5 -5
- package/frontend/src/components/ui/input.tsx +1 -1
- package/frontend/src/components/ui/textarea.tsx +1 -1
- package/frontend/src/pages/Act.tsx +58 -28
- package/frontend/src/pages/Ask.tsx +2 -197
- package/frontend/src/pages/Brain.tsx +108 -71
- package/frontend/src/pages/Capture.tsx +24 -24
- package/frontend/src/pages/Library.tsx +222 -32
- package/frontend/src/pages/System.tsx +56 -34
- package/frontend/src/routes.ts +16 -25
- package/frontend/src/store/appStore.ts +8 -1
- package/frontend/src/styles.css +1663 -36
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +107 -18
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/model_compat.py +250 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +136 -32
- package/latticeai/services/model_catalog.py +2 -2
- package/latticeai/services/model_recommendation.py +8 -1
- package/latticeai/services/model_runtime.py +18 -3
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +12 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-By-G-Kay.css +2 -0
- package/static/app/assets/index-CJx6WuQH.js +336 -0
- package/static/app/assets/index-CJx6WuQH.js.map +1 -0
- package/static/app/index.html +4 -4
- package/static/manifest.json +1 -1
- package/static/app/assets/index-CHHal8Zl.css +0 -2
- package/static/app/assets/index-pdzil9ac.js +0 -333
- package/static/app/assets/index-pdzil9ac.js.map +0 -1
|
@@ -14,7 +14,7 @@ from datetime import datetime
|
|
|
14
14
|
from typing import Any, Callable, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
MULTI_AGENT_VERSION = "4.
|
|
17
|
+
MULTI_AGENT_VERSION = "4.6.0"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/models.py
CHANGED
|
@@ -135,6 +135,8 @@ def create_models_router(
|
|
|
135
135
|
loaded_ids: Optional[List[str]] = None,
|
|
136
136
|
current_id: Optional[str] = None,
|
|
137
137
|
) -> List[Dict[str, object]]:
|
|
138
|
+
from latticeai.core.model_compat import model_runtime_compatibility
|
|
139
|
+
|
|
138
140
|
engine_lookup = {str(engine.get("id") or ""): engine for engine in engines or []}
|
|
139
141
|
model_lookup: Dict[str, Dict[str, object]] = {}
|
|
140
142
|
for engine in engines or []:
|
|
@@ -147,30 +149,93 @@ def create_models_router(
|
|
|
147
149
|
for item in items:
|
|
148
150
|
short_id = str(item["id"]).lower()
|
|
149
151
|
aliases = MODEL_ENGINE_ALIASES.get(short_id) or {}
|
|
150
|
-
options: List[Dict[str,
|
|
152
|
+
options: List[Dict[str, object]] = []
|
|
151
153
|
for engine_name in ("local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"):
|
|
152
154
|
real = aliases.get(engine_name)
|
|
153
155
|
if not real:
|
|
154
156
|
continue
|
|
157
|
+
load_id = real if engine_name == "local_mlx" else f"{engine_name}:{real}"
|
|
158
|
+
engine_info = engine_lookup.get(engine_name) or {}
|
|
159
|
+
model_info = model_lookup.get(load_id) or model_lookup.get(real) or {}
|
|
160
|
+
option_loaded = load_id in loaded or real in loaded or current_id in {load_id, real}
|
|
161
|
+
option_runtime = model_runtime_compatibility(load_id, engine=engine_name)
|
|
162
|
+
option_supported = option_runtime.get("supported") is not False
|
|
163
|
+
option_pulled = bool(model_info.get("pulled"))
|
|
164
|
+
option_download_required = bool(item.get("pullable", True) and not option_pulled and not option_loaded)
|
|
155
165
|
options.append({
|
|
156
166
|
"engine": engine_name,
|
|
157
167
|
"model_id": real,
|
|
158
|
-
"load_id":
|
|
168
|
+
"load_id": load_id,
|
|
169
|
+
"installed": bool(engine_info.get("installed")),
|
|
170
|
+
"pulled": option_pulled,
|
|
171
|
+
"loaded": option_loaded,
|
|
172
|
+
"download_required": option_download_required,
|
|
173
|
+
"runtime_compatibility": option_runtime,
|
|
174
|
+
"runtime_supported": option_supported,
|
|
175
|
+
"runtime_label": str(option_runtime.get("preferred_runtime") or engine_info.get("name") or engine_name),
|
|
159
176
|
})
|
|
160
177
|
if not options:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
raw_id = str(item["id"])
|
|
179
|
+
engine_info = engine_lookup.get("local_mlx") or {}
|
|
180
|
+
model_info = model_lookup.get(raw_id) or {}
|
|
181
|
+
option_loaded = raw_id in loaded or current_id == raw_id
|
|
182
|
+
option_pulled = bool(model_info.get("pulled"))
|
|
183
|
+
runtime_compatibility = model_runtime_compatibility(str(item["id"]), engine="local_mlx")
|
|
184
|
+
options.append({
|
|
185
|
+
"engine": "local_mlx",
|
|
186
|
+
"model_id": item["id"],
|
|
187
|
+
"load_id": item["id"],
|
|
188
|
+
"installed": bool(engine_info.get("installed")),
|
|
189
|
+
"pulled": option_pulled,
|
|
190
|
+
"loaded": option_loaded,
|
|
191
|
+
"download_required": bool(item.get("pullable", True) and not option_pulled and not option_loaded),
|
|
192
|
+
"runtime_compatibility": runtime_compatibility,
|
|
193
|
+
"runtime_supported": runtime_compatibility.get("supported") is not False,
|
|
194
|
+
"runtime_label": str(runtime_compatibility.get("preferred_runtime") or "MLX"),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
def option_rank(option: Dict[str, object]) -> tuple[int, int, int, int]:
|
|
198
|
+
runtime_supported = bool(option.get("runtime_supported"))
|
|
199
|
+
installed = bool(option.get("installed"))
|
|
200
|
+
loaded_option = bool(option.get("loaded"))
|
|
201
|
+
ready_without_download = installed and not bool(option.get("download_required"))
|
|
202
|
+
return (
|
|
203
|
+
0 if runtime_supported else 1,
|
|
204
|
+
0 if loaded_option or ready_without_download else 1,
|
|
205
|
+
0 if installed else 1,
|
|
206
|
+
["local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"].index(str(option.get("engine") or "vllm")),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
primary_option = options[0]
|
|
210
|
+
primary_compatibility = dict(primary_option.get("runtime_compatibility") or {})
|
|
211
|
+
hard_primary_statuses = {
|
|
212
|
+
"runtime_update_needed",
|
|
213
|
+
"unsupported_format",
|
|
214
|
+
"repair_model",
|
|
215
|
+
"incomplete_download",
|
|
216
|
+
}
|
|
217
|
+
selected_option = (
|
|
218
|
+
primary_option
|
|
219
|
+
if primary_compatibility.get("supported") is False
|
|
220
|
+
and primary_compatibility.get("status") in hard_primary_statuses
|
|
221
|
+
else min(options, key=option_rank)
|
|
222
|
+
)
|
|
223
|
+
recommended_engine = str(selected_option["engine"])
|
|
224
|
+
load_id = str(selected_option["load_id"])
|
|
225
|
+
model_info = model_lookup.get(load_id) or model_lookup.get(str(selected_option.get("model_id") or "")) or {}
|
|
226
|
+
pulled = bool(selected_option.get("pulled") or model_info.get("pulled"))
|
|
227
|
+
is_loaded = bool(selected_option.get("loaded"))
|
|
228
|
+
engine_installed = bool(selected_option.get("installed"))
|
|
169
229
|
pullable = bool(item.get("pullable", True))
|
|
170
|
-
|
|
230
|
+
runtime_compatibility = dict(selected_option.get("runtime_compatibility") or {})
|
|
231
|
+
runtime_supported = runtime_compatibility.get("supported") is not False
|
|
232
|
+
download_required = bool(selected_option.get("download_required") and pullable and not is_loaded)
|
|
171
233
|
if is_loaded:
|
|
172
234
|
load_status = "loaded"
|
|
173
235
|
unavailable_reason = None
|
|
236
|
+
elif not runtime_supported:
|
|
237
|
+
load_status = str(runtime_compatibility.get("status") or "unsupported")
|
|
238
|
+
unavailable_reason = str(runtime_compatibility.get("user_message") or "This model is not supported by the installed runtime.")
|
|
174
239
|
elif not engine_installed:
|
|
175
240
|
load_status = "unavailable"
|
|
176
241
|
unavailable_reason = f"{engine_info.get('name') or recommended_engine} runtime is not installed."
|
|
@@ -196,9 +261,13 @@ def create_models_router(
|
|
|
196
261
|
"source_display_order": item.get("source_display_order"),
|
|
197
262
|
"pulled": pulled,
|
|
198
263
|
"download_required": download_required,
|
|
199
|
-
"load_available": is_loaded or (engine_installed and not download_required),
|
|
264
|
+
"load_available": is_loaded or (runtime_supported and engine_installed and not download_required),
|
|
200
265
|
"load_status": load_status,
|
|
201
266
|
"unavailable_reason": unavailable_reason,
|
|
267
|
+
"runtime_compatibility": runtime_compatibility,
|
|
268
|
+
"recovery_guidance": runtime_compatibility.get("recovery_guidance") or [],
|
|
269
|
+
"alternative_recommendations": runtime_compatibility.get("alternatives") or [],
|
|
270
|
+
"runtime_label": selected_option.get("runtime_label"),
|
|
202
271
|
}
|
|
203
272
|
base["engine_options"] = options
|
|
204
273
|
base["recommended_engine"] = recommended_engine
|
|
@@ -272,10 +341,20 @@ def create_models_router(
|
|
|
272
341
|
@router.post("/engines/prepare-model")
|
|
273
342
|
async def engines_prepare_model(req: PrepareModelRequest, request: Request):
|
|
274
343
|
require_user(request)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
344
|
+
try:
|
|
345
|
+
return await prepare_and_load_model(
|
|
346
|
+
req.model, request, engine=req.engine, user_email=req.user_email,
|
|
347
|
+
allow_download=req.allow_download,
|
|
348
|
+
)
|
|
349
|
+
except HTTPException:
|
|
350
|
+
raise
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
from latticeai.core.model_compat import friendly_model_runtime_error
|
|
353
|
+
|
|
354
|
+
raise HTTPException(
|
|
355
|
+
status_code=500,
|
|
356
|
+
detail=friendly_model_runtime_error(exc, model_id=req.model, engine=req.engine),
|
|
357
|
+
)
|
|
279
358
|
|
|
280
359
|
@router.post("/engines/prepare-model/stream")
|
|
281
360
|
async def engines_prepare_model_stream(req: PrepareModelRequest, request: Request):
|
|
@@ -295,9 +374,11 @@ def create_models_router(
|
|
|
295
374
|
})
|
|
296
375
|
except Exception as exc:
|
|
297
376
|
logging.exception("model prepare stream failed")
|
|
377
|
+
from latticeai.core.model_compat import friendly_model_runtime_error
|
|
378
|
+
|
|
298
379
|
yield sse_event("error", {
|
|
299
380
|
"status_code": 500,
|
|
300
|
-
"detail":
|
|
381
|
+
"detail": friendly_model_runtime_error(exc, model_id=req.model, engine=req.engine),
|
|
301
382
|
})
|
|
302
383
|
|
|
303
384
|
return StreamingResponse(
|
|
@@ -356,6 +437,8 @@ def create_models_router(
|
|
|
356
437
|
@router.post("/models/load")
|
|
357
438
|
async def load_model(req: LoadModelRequest, request: Request):
|
|
358
439
|
try:
|
|
440
|
+
from latticeai.core.model_compat import friendly_model_runtime_error, model_runtime_compatibility
|
|
441
|
+
|
|
359
442
|
model_id = req.model_id
|
|
360
443
|
requested_engine = req.engine or (model_id.split(":", 1)[0] if ":" in model_id else "local_mlx")
|
|
361
444
|
if IS_PUBLIC_MODE and not ALLOW_LOCAL_MODELS and requested_engine in {"local_mlx", "mlx"}:
|
|
@@ -363,6 +446,9 @@ def create_models_router(
|
|
|
363
446
|
status_code=400,
|
|
364
447
|
detail="Public mode blocks local MLX model loading. Use openai:, openrouter:, groq:, together:, or set LATTICEAI_ALLOW_LOCAL_MODELS=true.",
|
|
365
448
|
)
|
|
449
|
+
compatibility = model_runtime_compatibility(model_id, engine=requested_engine)
|
|
450
|
+
if compatibility.get("supported") is False:
|
|
451
|
+
raise HTTPException(status_code=400, detail=compatibility)
|
|
366
452
|
return await prepare_and_load_model(
|
|
367
453
|
model_id, request, engine=req.engine, user_email=req.user_email,
|
|
368
454
|
adapter_path=req.adapter_path, draft_model_id=req.draft_model_id,
|
|
@@ -371,7 +457,10 @@ def create_models_router(
|
|
|
371
457
|
except HTTPException:
|
|
372
458
|
raise
|
|
373
459
|
except Exception as e:
|
|
374
|
-
raise HTTPException(
|
|
460
|
+
raise HTTPException(
|
|
461
|
+
status_code=500,
|
|
462
|
+
detail=friendly_model_runtime_error(e, model_id=req.model_id, engine=req.engine),
|
|
463
|
+
)
|
|
375
464
|
|
|
376
465
|
@router.post("/models/switch/{model_id:path}")
|
|
377
466
|
async def switch_model(model_id: str, request: Request):
|
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import importlib.util
|
|
16
|
+
import json
|
|
15
17
|
import logging
|
|
16
18
|
import re
|
|
17
19
|
import threading
|
|
18
20
|
import time
|
|
19
21
|
from dataclasses import dataclass, asdict
|
|
22
|
+
from pathlib import Path
|
|
20
23
|
from typing import Any, Dict, List, Optional, Tuple
|
|
21
24
|
|
|
22
25
|
logger = logging.getLogger(__name__)
|
|
@@ -119,6 +122,251 @@ def get_model_profile(model_id: str, engine: Optional[str] = None) -> Dict[str,
|
|
|
119
122
|
return base
|
|
120
123
|
|
|
121
124
|
|
|
125
|
+
# ── Runtime compatibility checks ─────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
GEMMA4_MLX_UNIFIED_MODULE = "mlx_vlm.models.gemma4_unified"
|
|
128
|
+
GEMMA4_MLX_LM_MODULES = ("mlx_lm.models.gemma4", "mlx_lm.models.gemma4_text")
|
|
129
|
+
GEMMA4_UNIFIED_ID_PATTERN = re.compile(r"gemma[-_/ ]?4[-_/ ]?12b", re.I)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _module_available(module: str) -> bool:
|
|
133
|
+
try:
|
|
134
|
+
return importlib.util.find_spec(module) is not None
|
|
135
|
+
except (ImportError, ModuleNotFoundError, AttributeError, ValueError):
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_gemma4(model_id: str) -> bool:
|
|
140
|
+
raw = str(model_id or "").lower()
|
|
141
|
+
return bool(re.search(r"gemma[-_/ ]?4", raw))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _hf_model_dir(repo_id: str) -> Path:
|
|
145
|
+
return Path.home() / ".ltcai" / "hf-models" / repo_id.replace("/", "__")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _local_model_type(model_id: str) -> Optional[str]:
|
|
149
|
+
raw = str(model_id or "").strip()
|
|
150
|
+
if "gemma4_unified" in raw.lower():
|
|
151
|
+
return "gemma4_unified"
|
|
152
|
+
candidates = []
|
|
153
|
+
explicit = Path(raw).expanduser()
|
|
154
|
+
if raw and explicit.exists():
|
|
155
|
+
candidates.append(explicit / "config.json")
|
|
156
|
+
candidates.append(_hf_model_dir(raw) / "config.json")
|
|
157
|
+
for config_path in candidates:
|
|
158
|
+
try:
|
|
159
|
+
if config_path.exists():
|
|
160
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
161
|
+
model_type = str(data.get("model_type") or "").strip().lower()
|
|
162
|
+
if model_type:
|
|
163
|
+
return model_type
|
|
164
|
+
except Exception:
|
|
165
|
+
logger.debug("failed to read model config %s", config_path, exc_info=True)
|
|
166
|
+
if GEMMA4_UNIFIED_ID_PATTERN.search(raw):
|
|
167
|
+
return "gemma4_unified"
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _gemma4_runtime_candidates(raw_model_id: str) -> List[Dict[str, Any]]:
|
|
172
|
+
mlx_available = _module_available("mlx")
|
|
173
|
+
mlx_vlm_available = mlx_available and _module_available("mlx_vlm")
|
|
174
|
+
mlx_vlm_unified_available = mlx_vlm_available and _module_available(GEMMA4_MLX_UNIFIED_MODULE)
|
|
175
|
+
mlx_lm_available = mlx_available and _module_available("mlx_lm")
|
|
176
|
+
mlx_lm_gemma4_available = mlx_lm_available and any(_module_available(module) for module in GEMMA4_MLX_LM_MODULES)
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
"engine": "local_mlx",
|
|
180
|
+
"runtime": "MLX-VLM",
|
|
181
|
+
"load_id": raw_model_id,
|
|
182
|
+
"available": mlx_vlm_available,
|
|
183
|
+
"supports_gemma4_unified": mlx_vlm_unified_available,
|
|
184
|
+
"role": "v3_primary",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"engine": "local_mlx",
|
|
188
|
+
"runtime": "MLX-LM",
|
|
189
|
+
"load_id": raw_model_id,
|
|
190
|
+
"available": mlx_lm_gemma4_available,
|
|
191
|
+
"role": "v3_text_fallback",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"engine": "ollama",
|
|
195
|
+
"runtime": "Ollama GGUF",
|
|
196
|
+
"load_id": "ollama:hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M",
|
|
197
|
+
"available": None,
|
|
198
|
+
"role": "gguf_local_server",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"engine": "lmstudio",
|
|
202
|
+
"runtime": "LM Studio GGUF",
|
|
203
|
+
"load_id": "lmstudio:ggml-org/gemma-4-12B-it-GGUF",
|
|
204
|
+
"available": None,
|
|
205
|
+
"role": "gguf_local_server",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"engine": "llamacpp",
|
|
209
|
+
"runtime": "llama.cpp GGUF",
|
|
210
|
+
"load_id": "llamacpp:ggml-org/gemma-4-12B-it-GGUF",
|
|
211
|
+
"available": None,
|
|
212
|
+
"role": "gguf_local_server",
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def model_runtime_compatibility(model_id: str, engine: Optional[str] = None) -> Dict[str, Any]:
|
|
218
|
+
"""Return a lightweight pre-load runtime compatibility signal.
|
|
219
|
+
|
|
220
|
+
This intentionally checks only known fast failure modes. The loader and
|
|
221
|
+
smoke test remain the final authority, but the UI must not present a model
|
|
222
|
+
as ready when the installed runtime is known to lack its required loader.
|
|
223
|
+
"""
|
|
224
|
+
normalized_engine = (engine or "").strip().lower()
|
|
225
|
+
if normalized_engine in {"", "mlx"}:
|
|
226
|
+
normalized_engine = "local_mlx"
|
|
227
|
+
raw_model_id = str(model_id or "")
|
|
228
|
+
if raw_model_id.startswith(("local_mlx:", "mlx:")):
|
|
229
|
+
raw_model_id = raw_model_id.split(":", 1)[1]
|
|
230
|
+
|
|
231
|
+
payload: Dict[str, Any] = {
|
|
232
|
+
"model_id": raw_model_id,
|
|
233
|
+
"engine": normalized_engine or None,
|
|
234
|
+
"family": detect_model_family(raw_model_id),
|
|
235
|
+
"status": "supported",
|
|
236
|
+
"supported": True,
|
|
237
|
+
"checked": True,
|
|
238
|
+
"runtime": None,
|
|
239
|
+
"preferred_runtime": None,
|
|
240
|
+
"runtime_candidates": [],
|
|
241
|
+
"missing_components": [],
|
|
242
|
+
"user_message": None,
|
|
243
|
+
"recovery_guidance": [],
|
|
244
|
+
"alternatives": [],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if normalized_engine != "local_mlx" or not _is_gemma4(raw_model_id):
|
|
248
|
+
return payload
|
|
249
|
+
|
|
250
|
+
candidates = _gemma4_runtime_candidates(raw_model_id)
|
|
251
|
+
payload["runtime_candidates"] = candidates
|
|
252
|
+
payload["runtime"] = "MLX-VLM"
|
|
253
|
+
payload["preferred_runtime"] = "MLX-VLM"
|
|
254
|
+
model_type = _local_model_type(raw_model_id)
|
|
255
|
+
if model_type:
|
|
256
|
+
payload["model_type"] = model_type
|
|
257
|
+
|
|
258
|
+
mlx_available = _module_available("mlx")
|
|
259
|
+
mlx_vlm_available = _module_available("mlx_vlm")
|
|
260
|
+
mlx_lm_available = any(bool(candidate.get("available")) for candidate in candidates if candidate.get("runtime") == "MLX-LM")
|
|
261
|
+
|
|
262
|
+
if not mlx_available or not (mlx_vlm_available or mlx_lm_available):
|
|
263
|
+
payload.update({
|
|
264
|
+
"status": "runtime_not_installed",
|
|
265
|
+
"checked": False,
|
|
266
|
+
"supported": True,
|
|
267
|
+
"user_message": (
|
|
268
|
+
"Install the local MLX runtime before loading Gemma 4, or choose "
|
|
269
|
+
"the Gemma 4 GGUF route through Ollama, LM Studio, or llama.cpp."
|
|
270
|
+
),
|
|
271
|
+
"alternatives": candidates[2:],
|
|
272
|
+
})
|
|
273
|
+
return payload
|
|
274
|
+
|
|
275
|
+
if model_type == "gemma4_unified" and not _module_available(GEMMA4_MLX_UNIFIED_MODULE):
|
|
276
|
+
payload.update({
|
|
277
|
+
"status": "runtime_update_needed",
|
|
278
|
+
"supported": False,
|
|
279
|
+
"reason_code": "mlx_vlm_missing_gemma4_unified_model",
|
|
280
|
+
"model_type": "gemma4_unified",
|
|
281
|
+
"missing_components": [GEMMA4_MLX_UNIFIED_MODULE],
|
|
282
|
+
"user_message": (
|
|
283
|
+
"Gemma 4 12B uses the gemma4_unified MLX format. The installed "
|
|
284
|
+
"MLX-VLM runtime does not include that loader, so this local "
|
|
285
|
+
"model cannot load until MLX-VLM is updated."
|
|
286
|
+
),
|
|
287
|
+
"recovery_guidance": [
|
|
288
|
+
"Update the MLX runtime from Library/System setup, or run: pip install --upgrade 'mlx-vlm>=0.6.3'.",
|
|
289
|
+
"After the runtime update, re-open Models so Lattice can re-check this model.",
|
|
290
|
+
"Use Gemma 4 26B A4B locally or Gemma 4 12B GGUF through Ollama, LM Studio, or llama.cpp until then.",
|
|
291
|
+
],
|
|
292
|
+
"alternatives": [
|
|
293
|
+
{"id": "mlx-community/gemma-4-26b-a4b-it-4bit", "name": "Gemma 4 26B A4B", "engine": "local_mlx"},
|
|
294
|
+
{"id": "ollama:hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M", "name": "Gemma 4 12B GGUF", "engine": "ollama"},
|
|
295
|
+
{"id": "lmstudio:ggml-org/gemma-4-12B-it-GGUF", "name": "Gemma 4 12B GGUF", "engine": "lmstudio"},
|
|
296
|
+
],
|
|
297
|
+
"action": "Runtime update needed",
|
|
298
|
+
})
|
|
299
|
+
return payload
|
|
300
|
+
|
|
301
|
+
if mlx_vlm_available:
|
|
302
|
+
return payload
|
|
303
|
+
|
|
304
|
+
payload.update({
|
|
305
|
+
"status": "fallback_available",
|
|
306
|
+
"supported": True,
|
|
307
|
+
"reason_code": "mlx_vlm_missing_gemma4_standard_runtime",
|
|
308
|
+
"missing_components": ["mlx_vlm"],
|
|
309
|
+
"user_message": (
|
|
310
|
+
"MLX-VLM is not available for this Gemma 4 model. Lattice can use "
|
|
311
|
+
"the MLX-LM text fallback or a Gemma 4 GGUF local runtime."
|
|
312
|
+
),
|
|
313
|
+
"recovery_guidance": [
|
|
314
|
+
"Use the MLX-LM text fallback for text chat.",
|
|
315
|
+
"Use a Gemma 4 GGUF model through Ollama, LM Studio, or llama.cpp if the MLX route fails.",
|
|
316
|
+
],
|
|
317
|
+
"alternatives": [
|
|
318
|
+
{"id": candidate["load_id"], "name": candidate["runtime"], "engine": candidate["engine"]}
|
|
319
|
+
for candidate in candidates
|
|
320
|
+
if candidate.get("role") != "v3_primary"
|
|
321
|
+
],
|
|
322
|
+
})
|
|
323
|
+
if mlx_lm_available:
|
|
324
|
+
payload["preferred_runtime"] = "MLX-LM fallback"
|
|
325
|
+
return payload
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def friendly_model_runtime_error(
|
|
329
|
+
error: BaseException | str,
|
|
330
|
+
*,
|
|
331
|
+
model_id: Optional[str] = None,
|
|
332
|
+
engine: Optional[str] = None,
|
|
333
|
+
) -> Dict[str, Any]:
|
|
334
|
+
"""Convert loader exceptions into end-user recoverable error payloads."""
|
|
335
|
+
raw = str(error or "")
|
|
336
|
+
compat = model_runtime_compatibility(model_id or raw, engine=engine)
|
|
337
|
+
if not compat.get("supported", True):
|
|
338
|
+
return {
|
|
339
|
+
"status": compat.get("status") or "unsupported",
|
|
340
|
+
"model_id": model_id,
|
|
341
|
+
"engine": engine,
|
|
342
|
+
"user_message": compat.get("user_message") or (
|
|
343
|
+
"The selected model is not supported by the installed local runtime."
|
|
344
|
+
),
|
|
345
|
+
"recovery_guidance": compat.get("recovery_guidance") or [
|
|
346
|
+
"Choose a recommended alternative model.",
|
|
347
|
+
"Update the local runtime and try validation again.",
|
|
348
|
+
],
|
|
349
|
+
"alternatives": compat.get("alternatives") or [],
|
|
350
|
+
"missing_components": compat.get("missing_components") or [],
|
|
351
|
+
"action": compat.get("action"),
|
|
352
|
+
"reason_code": compat.get("reason_code") or "runtime_model_type_unsupported",
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
"status": "load_failed",
|
|
356
|
+
"model_id": model_id,
|
|
357
|
+
"engine": engine,
|
|
358
|
+
"user_message": (
|
|
359
|
+
"The model could not be loaded. Check that the runtime is installed, "
|
|
360
|
+
"the model files are present, and try a recommended alternative if the issue continues."
|
|
361
|
+
),
|
|
362
|
+
"recovery_guidance": [
|
|
363
|
+
"Open Models and run the setup flow again.",
|
|
364
|
+
"Confirm downloads were explicitly allowed for models that are not on this computer.",
|
|
365
|
+
"Try a recommended smaller local model if memory is low.",
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
122
370
|
# ── Postprocessing ────────────────────────────────────────────────────────────
|
|
123
371
|
|
|
124
372
|
BAD_MARKERS = [
|
|
@@ -378,7 +626,9 @@ __all__ = [
|
|
|
378
626
|
"FAMILY_PROFILES",
|
|
379
627
|
"CompatProfile",
|
|
380
628
|
"detect_model_family",
|
|
629
|
+
"friendly_model_runtime_error",
|
|
381
630
|
"get_model_profile",
|
|
631
|
+
"model_runtime_compatibility",
|
|
382
632
|
"fast_postprocess",
|
|
383
633
|
"validate_smoke_response",
|
|
384
634
|
"classify_smoke_response",
|
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
WORKSPACE_OS_VERSION = "4.
|
|
22
|
+
WORKSPACE_OS_VERSION = "4.6.0"
|
|
23
23
|
|
|
24
24
|
# Workspace types separate single-user Personal workspaces from shared
|
|
25
25
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|