ltcai 5.0.0 → 5.2.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 +75 -55
- package/docs/CHANGELOG.md +96 -2354
- package/docs/TRUST_MODEL.md +66 -0
- package/docs/V4_1_VALIDATION_REPORT.md +1 -1
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +2 -2
- package/docs/V4_5_1_VALIDATION_REPORT.md +2 -1
- package/docs/WHY_LATTICE.md +54 -0
- package/frontend/src/App.tsx +1 -1
- package/frontend/src/components/primitives.tsx +1 -1
- package/frontend/src/i18n.ts +6 -4
- package/frontend/src/pages/Library.tsx +29 -4
- package/frontend/src/pages/System.tsx +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/portability.py +11 -7
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +19 -11
- package/latticeai/api/marketplace.py +2 -2
- package/latticeai/api/models.py +26 -4
- package/latticeai/api/security_dashboard.py +3 -15
- package/latticeai/api/static_routes.py +16 -0
- package/latticeai/app_factory.py +114 -40
- package/latticeai/core/audit.py +3 -1
- package/latticeai/core/builtin_hooks.py +7 -9
- package/latticeai/core/logging_safety.py +5 -21
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/security.py +67 -9
- package/latticeai/core/workspace_os.py +18 -4
- package/latticeai/services/model_capability_registry.py +483 -0
- package/latticeai/services/model_catalog.py +99 -96
- package/latticeai/services/model_recommendation.py +12 -1
- package/package.json +2 -2
- package/scripts/clean_release_artifacts.mjs +16 -1
- package/scripts/com.pts.claudecode.discord.plist +31 -0
- package/scripts/pts-claudecode-discord-bridge.mjs +189 -0
- package/scripts/run_integration_tests.mjs +91 -0
- package/scripts/start-pts-claudecode-discord.sh +51 -0
- package/scripts/verify_hf_model_registry.py +308 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +3 -2
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-CQmHhk8Q.css +2 -0
- package/static/app/assets/{index-FR1UZkCD.js → index-DsnfomFs.js} +2 -2
- package/static/app/assets/index-DsnfomFs.js.map +1 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-DuYYT2oh.css +0 -2
- package/static/app/assets/index-FR1UZkCD.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Static local-model catalog, engine installers, and family-version filtering.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
5.2.0: Now sources the rich ENGINE_MODEL_CATALOG from the structured
|
|
4
|
+
model_capability_registry (single source of truth with HF verification,
|
|
5
|
+
download/load strategy, hardware, license, modality). Legacy flat shapes
|
|
6
|
+
preserved exactly for all downstream (recommendation, api, runtime, frontend).
|
|
7
|
+
|
|
8
|
+
The old inline _model() + ENGINE_MODEL_CATALOG data has been moved into
|
|
9
|
+
latticeai/services/model_capability_registry.py (see there for full 5.2 fields).
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -15,6 +15,16 @@ import re
|
|
|
15
15
|
import sys
|
|
16
16
|
from typing import Dict, List, Optional
|
|
17
17
|
|
|
18
|
+
# 5.2.0: Delegate catalog data to the structured capability registry (rich + verified).
|
|
19
|
+
# This keeps backward compat for every `from ...model_catalog import ENGINE_MODEL_CATALOG`.
|
|
20
|
+
from latticeai.services.model_capability_registry import (
|
|
21
|
+
build_engine_model_catalog as _build_engine_model_catalog,
|
|
22
|
+
get_all_capabilities as _get_all_capabilities,
|
|
23
|
+
get_capability as _get_capability,
|
|
24
|
+
get_verified_models as _get_verified_models,
|
|
25
|
+
LOCAL_MLX_MODELS as _LOCAL_MLX_MODELS,
|
|
26
|
+
)
|
|
27
|
+
|
|
18
28
|
ENGINE_INSTALLERS = {
|
|
19
29
|
"local_mlx": {
|
|
20
30
|
"command": [sys.executable, "-m", "pip", "install", "--upgrade", "mlx-vlm>=0.6.3", "mlx-lm", "huggingface_hub[cli]"],
|
|
@@ -61,95 +71,22 @@ ENGINE_INSTALLERS = {
|
|
|
61
71
|
},
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"name": name,
|
|
81
|
-
"model_name": clean_model_name,
|
|
82
|
-
"family": family,
|
|
83
|
-
"tag": tag,
|
|
84
|
-
"size": size,
|
|
85
|
-
"pullable": pullable,
|
|
86
|
-
"modality": "multimodal",
|
|
87
|
-
"source_country": source_country,
|
|
88
|
-
"source_company": source_company,
|
|
89
|
-
"execution_method": execution_method,
|
|
90
|
-
"run_location": "내 컴퓨터에서만 실행",
|
|
91
|
-
"internet_requirement": internet_requirement,
|
|
92
|
-
"source_display_order": [
|
|
93
|
-
"source_country",
|
|
94
|
-
"source_company",
|
|
95
|
-
"execution_method",
|
|
96
|
-
"internet_requirement",
|
|
97
|
-
"model_name",
|
|
98
|
-
],
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
_RUNS_ON_THIS_COMPUTER = "내 컴퓨터에서만 실행"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
ENGINE_MODEL_CATALOG = {
|
|
106
|
-
"local_mlx": [
|
|
107
|
-
_model("mlx-community/gemma-4-e2b-4bit", "Gemma 4 E2B Base", "Gemma 4", "local-vlm", "3.6GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
108
|
-
_model("mlx-community/gemma-4-e2b-it-4bit", "Gemma 4 E2B Instruct", "Gemma 4", "local-vlm", "3.6GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
109
|
-
_model("mlx-community/gemma-4-e4b-4bit", "Gemma 4 E4B Base", "Gemma 4", "local-vlm", "5.2GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
110
|
-
_model("mlx-community/gemma-4-e4b-it-4bit", "Gemma 4 E4B Instruct", "Gemma 4", "local-vlm", "5.2GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
111
|
-
_model("mlx-community/gemma-4-12b-it-4bit", "Gemma 4 12B Instruct", "Gemma 4", "local-vlm", "7.6GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
112
|
-
_model("mlx-community/gemma-4-26b-a4b-it-4bit", "Gemma 4 26B A4B Instruct", "Gemma 4", "local-vlm", "15.6GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
113
|
-
_model("mlx-community/gemma-4-31b-it-4bit", "Gemma 4 31B Instruct", "Gemma 4", "local-vlm", "18.4GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
114
|
-
_model("mlx-community/Qwen3-VL-4B-Instruct-4bit", "Qwen3-VL 4B", "Qwen3-VL", "local-vlm", "2.7GB", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
115
|
-
_model("mlx-community/Qwen3-VL-8B-Instruct-4bit", "Qwen3-VL 8B", "Qwen3-VL", "local-vlm", "4.8GB", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
116
|
-
_model("mlx-community/Qwen3-VL-30B-A3B-Instruct-4bit", "Qwen3-VL 30B A3B", "Qwen3-VL", "local-vlm", "18GB", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
117
|
-
_model("mlx-community/Llama-4-Scout-17B-16E-Instruct-4bit", "Llama 4 Scout 17B 16E", "Llama 4", "local-vlm", "11.8GB", source_country="미국", source_company="Meta", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
118
|
-
],
|
|
119
|
-
"ollama": [
|
|
120
|
-
_model("ollama:qwen3-vl:4b", "Qwen3-VL 4B via Ollama", "Qwen3-VL", "local-vlm", "pull required", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
121
|
-
_model("ollama:qwen3-vl:8b", "Qwen3-VL 8B via Ollama", "Qwen3-VL", "local-vlm", "pull required", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
122
|
-
_model("ollama:qwen3-vl:30b", "Qwen3-VL 30B via Ollama", "Qwen3-VL", "local-vlm", "pull required", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
123
|
-
_model("ollama:hf.co/ggml-org/gemma-4-12B-it-GGUF:Q4_K_M", "Gemma 4 12B Q4 via Ollama", "Gemma 4", "local-vlm", "7.9GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
124
|
-
_model("ollama:hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M", "Gemma 4 31B Q4 via Ollama", "Gemma 4", "local-vlm", "18.7GB", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
125
|
-
_model("ollama:hf.co/ggml-org/Llama-4-Scout-17B-16E-Instruct-GGUF:Q4_K_M", "Llama 4 Scout Q4 via Ollama", "Llama 4", "local-vlm", "12GB", source_country="미국", source_company="Meta", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
126
|
-
],
|
|
127
|
-
"vllm": [
|
|
128
|
-
_model("vllm:Qwen/Qwen3-VL-4B-Instruct", "Qwen3-VL 4B via vLLM", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
129
|
-
_model("vllm:Qwen/Qwen3-VL-8B-Instruct", "Qwen3-VL 8B via vLLM", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
130
|
-
_model("vllm:Qwen/Qwen3-VL-30B-A3B-Instruct", "Qwen3-VL 30B A3B via vLLM", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
131
|
-
_model("vllm:google/gemma-4-12b-it", "Gemma 4 12B via vLLM", "Gemma 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
132
|
-
_model("vllm:suitch/gemma-4-31B-it-4bit", "Gemma 4 31B via vLLM", "Gemma 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
133
|
-
_model("vllm:meta-llama/Llama-4-Scout-17B-16E-Instruct", "Llama 4 Scout via vLLM", "Llama 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Meta", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
134
|
-
],
|
|
135
|
-
"lmstudio": [
|
|
136
|
-
_model("lmstudio:Qwen/Qwen3-VL-4B-Instruct", "Qwen3-VL 4B via LM Studio", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
137
|
-
_model("lmstudio:Qwen/Qwen3-VL-8B-Instruct", "Qwen3-VL 8B via LM Studio", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
138
|
-
_model("lmstudio:Qwen/Qwen3-VL-30B-A3B-Instruct", "Qwen3-VL 30B A3B via LM Studio", "Qwen3-VL", "local-vlm", "실행 도구에서 관리", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
139
|
-
_model("lmstudio:ggml-org/gemma-4-12B-it-GGUF", "Gemma 4 12B 4-bit via LM Studio", "Gemma 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
140
|
-
_model("lmstudio:ggml-org/gemma-4-31B-it-GGUF", "Gemma 4 31B 4-bit via LM Studio", "Gemma 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
141
|
-
_model("lmstudio:meta-llama/Llama-4-Scout-17B-16E-Instruct", "Llama 4 Scout via LM Studio", "Llama 4", "local-vlm", "실행 도구에서 관리", source_country="미국", source_company="Meta", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
142
|
-
],
|
|
143
|
-
"llamacpp": [
|
|
144
|
-
_model("llamacpp:Qwen/Qwen3-VL-4B-Instruct-GGUF", "Qwen3-VL 4B GGUF via llama.cpp", "Qwen3-VL", "gguf-vlm", "gguf", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
145
|
-
_model("llamacpp:Qwen/Qwen3-VL-8B-Instruct-GGUF", "Qwen3-VL 8B GGUF via llama.cpp", "Qwen3-VL", "gguf-vlm", "gguf", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
146
|
-
_model("llamacpp:Qwen/Qwen3-VL-30B-A3B-Instruct-GGUF", "Qwen3-VL 30B GGUF via llama.cpp", "Qwen3-VL", "gguf-vlm", "gguf", source_country="중국", source_company="Alibaba", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
147
|
-
_model("llamacpp:ggml-org/gemma-4-12B-it-GGUF", "Gemma 4 12B GGUF via llama.cpp", "Gemma 4", "gguf-vlm", "gguf", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
148
|
-
_model("llamacpp:ggml-org/gemma-4-31B-it-GGUF", "Gemma 4 31B GGUF via llama.cpp", "Gemma 4", "gguf-vlm", "gguf", source_country="미국", source_company="Google", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
149
|
-
_model("llamacpp:ggml-org/Llama-4-Scout-17B-16E-Instruct-GGUF", "Llama 4 Scout GGUF via llama.cpp", "Llama 4", "gguf-vlm", "gguf", source_country="미국", source_company="Meta", execution_method=_RUNS_ON_THIS_COMPUTER),
|
|
150
|
-
],
|
|
151
|
-
}
|
|
152
|
-
|
|
74
|
+
# 5.2.0 delegation: the rich catalog (with verification, hf_repo_id, strategies, hardware, license etc)
|
|
75
|
+
# is defined in model_capability_registry. We build the legacy-shaped ENGINE_MODEL_CATALOG here
|
|
76
|
+
# at import time so every existing consumer (runtime, api, recommendation, tests) is unaffected.
|
|
77
|
+
#
|
|
78
|
+
# The *raw* registry projection keeps every capability (incl. legacy generations
|
|
79
|
+
# like Gemma 3 / Qwen2.5-VL / Pixtral) for transparency + HF verification. The
|
|
80
|
+
# user-facing ENGINE_MODEL_CATALOG below is then narrowed to the aggressive 5.2.0
|
|
81
|
+
# policy (latest family generations only, no text-only/legacy weights) and the
|
|
82
|
+
# engine-specific ids/sizes are normalised. See `_finalize_engine_catalog`.
|
|
83
|
+
_RAW_ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, object]]] = _build_engine_model_catalog()
|
|
84
|
+
# Filled in at module end once the blocklist, alias map and family-version filter
|
|
85
|
+
# are all defined; declared here so the public name exists for static readers.
|
|
86
|
+
ENGINE_MODEL_CATALOG: Dict[str, List[Dict[str, object]]] = {}
|
|
87
|
+
|
|
88
|
+
# Historical aliases preserved (used by _recommended_with_engine_options and resolution).
|
|
89
|
+
# These can be enriched later from registry if needed; kept verbatim for safety.
|
|
153
90
|
MODEL_ENGINE_ALIASES = {
|
|
154
91
|
"gemma-4-12b-it-4bit": {
|
|
155
92
|
"local_mlx": "mlx-community/gemma-4-12b-it-4bit",
|
|
@@ -202,6 +139,14 @@ MODEL_ENGINE_ALIASES = {
|
|
|
202
139
|
},
|
|
203
140
|
}
|
|
204
141
|
|
|
142
|
+
# Also expose registry helpers directly from here for consumers who want the rich objects
|
|
143
|
+
get_all_capabilities = _get_all_capabilities
|
|
144
|
+
get_capability = _get_capability
|
|
145
|
+
get_verified_models = _get_verified_models
|
|
146
|
+
|
|
147
|
+
# Convenience re-export for tests / places that did `from ...model_catalog import LOCAL_MLX_MODELS`
|
|
148
|
+
LOCAL_MLX_MODELS = _LOCAL_MLX_MODELS # type: ignore[name-defined]
|
|
149
|
+
|
|
205
150
|
_VERSIONED_MODEL_PATTERNS = (
|
|
206
151
|
("gemma", re.compile(r"\bgemma[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
|
|
207
152
|
("qwen", re.compile(r"\bqwen[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
|
|
@@ -239,3 +184,61 @@ def filter_lower_family_versions(models: List[Dict[str, object]]) -> List[Dict[s
|
|
|
239
184
|
model for model, version_info in detected
|
|
240
185
|
if not version_info or version_info[1] >= max_versions.get(version_info[0], version_info[1])
|
|
241
186
|
]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ── 5.2.0 user-facing catalog assembly ────────────────────────────────────────
|
|
190
|
+
# Legacy/text-only generations stay in the capability registry (for transparency
|
|
191
|
+
# and HF verification) but must never be surfaced in the model picker. Anything
|
|
192
|
+
# whose id contains one of these fragments is dropped from ENGINE_MODEL_CATALOG.
|
|
193
|
+
_BLOCKED_CATALOG_FRAGMENTS = (
|
|
194
|
+
"gemma-3", "gemma3", "gemma-2", "gemma2",
|
|
195
|
+
"qwen2.5", "qwen-2.5", "qwen2-5",
|
|
196
|
+
"llama-3", "llama3.2", "llama-3.2",
|
|
197
|
+
"pixtral", "mistral",
|
|
198
|
+
"smollm", "gpt-oss", "phi-",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_blocked_catalog_id(model: Dict[str, object]) -> bool:
|
|
203
|
+
ident = str(model.get("id") or "").lower()
|
|
204
|
+
return any(fragment in ident for fragment in _BLOCKED_CATALOG_FRAGMENTS)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _normalize_engine_entry(engine: str, model: Dict[str, object]) -> Dict[str, object]:
|
|
208
|
+
"""Apply historical engine-specific id + size conventions to a raw entry.
|
|
209
|
+
|
|
210
|
+
* Non-MLX engines resolve to their canonical packaged id via
|
|
211
|
+
:data:`MODEL_ENGINE_ALIASES` (e.g. ollama → ``hf.co/ggml-org/...GGUF``).
|
|
212
|
+
* Server / tool-managed engines advertise no fixed on-disk size, so the
|
|
213
|
+
execution tool validates the exact weights at pull time.
|
|
214
|
+
"""
|
|
215
|
+
if engine == "local_mlx":
|
|
216
|
+
return model
|
|
217
|
+
entry = dict(model)
|
|
218
|
+
hf_repo = str(entry.get("hf_repo_id") or "")
|
|
219
|
+
short = hf_repo.split("/")[-1].lower()
|
|
220
|
+
aliases = MODEL_ENGINE_ALIASES.get(short) or MODEL_ENGINE_ALIASES.get(hf_repo.lower())
|
|
221
|
+
mapped = aliases.get(engine) if aliases else None
|
|
222
|
+
if mapped:
|
|
223
|
+
entry["id"] = f"{engine}:{mapped}"
|
|
224
|
+
# Tool-managed engines (ollama/vllm/lmstudio/llamacpp) pull on demand; the
|
|
225
|
+
# registry's MLX on-disk size does not apply to them.
|
|
226
|
+
entry["size"] = "실행 도구에서 관리"
|
|
227
|
+
return entry
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _finalize_engine_catalog(
|
|
231
|
+
raw: Dict[str, List[Dict[str, object]]],
|
|
232
|
+
) -> Dict[str, List[Dict[str, object]]]:
|
|
233
|
+
final: Dict[str, List[Dict[str, object]]] = {}
|
|
234
|
+
for engine, models in raw.items():
|
|
235
|
+
kept = [
|
|
236
|
+
_normalize_engine_entry(engine, m)
|
|
237
|
+
for m in models
|
|
238
|
+
if not _is_blocked_catalog_id(m)
|
|
239
|
+
]
|
|
240
|
+
final[engine] = filter_lower_family_versions(kept)
|
|
241
|
+
return final
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
ENGINE_MODEL_CATALOG = _finalize_engine_catalog(_RAW_ENGINE_MODEL_CATALOG)
|
|
@@ -112,7 +112,7 @@ def _classify_one(
|
|
|
112
112
|
else:
|
|
113
113
|
status, reason = NOT_RECOMMENDED, f"권장 메모리가 부족합니다 (~{need_gb:.0f} GB 필요, 현재 {ram_gb:.0f} GB)"
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
rich = {
|
|
116
116
|
"id": model.get("id"),
|
|
117
117
|
"name": model.get("name"),
|
|
118
118
|
"model_name": model.get("model_name") or model.get("name"),
|
|
@@ -131,7 +131,18 @@ def _classify_one(
|
|
|
131
131
|
"internet_requirement": model.get("internet_requirement"),
|
|
132
132
|
"source_display_order": model.get("source_display_order"),
|
|
133
133
|
"runtime_compatibility": runtime,
|
|
134
|
+
# 5.2+ user-focused transparency
|
|
135
|
+
"hf_repo_id": model.get("hf_repo_id"),
|
|
136
|
+
"quantization": model.get("quantization"),
|
|
137
|
+
"download_strategy": model.get("download_strategy"),
|
|
138
|
+
"load_strategy": model.get("load_strategy"),
|
|
139
|
+
"hardware": model.get("hardware"),
|
|
140
|
+
"license": model.get("license"),
|
|
141
|
+
"safety_notes": model.get("safety_notes"),
|
|
142
|
+
"verification": model.get("verification"),
|
|
143
|
+
"recommended_default": model.get("recommended_default", False),
|
|
134
144
|
}
|
|
145
|
+
return rich
|
|
135
146
|
|
|
136
147
|
|
|
137
148
|
def _family_rank(family: str) -> int:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Lattice AI — local-first Living Brain workspace (conversation, durable memory, hybrid search, agents, advanced graph exploration, portable encrypted brain archives)",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
|
|
31
31
|
"test": "node scripts/run_python.mjs -m pytest tests/ -v",
|
|
32
32
|
"test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
|
|
33
|
-
"test:integration": "node scripts/
|
|
33
|
+
"test:integration": "node scripts/run_integration_tests.mjs",
|
|
34
34
|
"test:visual": "playwright test",
|
|
35
35
|
"vercel:build": "node scripts/build_vercel_static.mjs",
|
|
36
36
|
"desktop:tauri": "tauri dev",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
const repo = join(import.meta.dirname, "..");
|
|
@@ -19,6 +19,21 @@ const targets = [
|
|
|
19
19
|
join(repo, "src-tauri", "target", "release", "bundle", "macos", "Lattice AI.app"),
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
+
const distDir = join(repo, "dist");
|
|
23
|
+
if (existsSync(distDir)) {
|
|
24
|
+
for (const name of readdirSync(distDir)) {
|
|
25
|
+
if (/^ltcai-\d+\.\d+\.\d+.*\.(whl|tar\.gz|vsix|tgz)$/.test(name)) {
|
|
26
|
+
targets.push(join(distDir, name));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const name of readdirSync(repo)) {
|
|
32
|
+
if (/^ltcai-\d+\.\d+\.\d+.*\.tgz$/.test(name)) {
|
|
33
|
+
targets.push(join(repo, name));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
for (const target of targets) {
|
|
23
38
|
if (existsSync(target)) {
|
|
24
39
|
rmSync(target, { recursive: true, force: true });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
3
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4
|
+
<plist version="1.0">
|
|
5
|
+
<dict>
|
|
6
|
+
<key>Label</key>
|
|
7
|
+
<string>com.pts.claudecode.discord</string>
|
|
8
|
+
<key>ProgramArguments</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>/opt/homebrew/bin/node</string>
|
|
11
|
+
<string>/Users/parktaesoo/.claude/bin/pts-claudecode-discord-bridge.mjs</string>
|
|
12
|
+
</array>
|
|
13
|
+
<key>RunAtLoad</key>
|
|
14
|
+
<true/>
|
|
15
|
+
<key>KeepAlive</key>
|
|
16
|
+
<true/>
|
|
17
|
+
<key>EnvironmentVariables</key>
|
|
18
|
+
<dict>
|
|
19
|
+
<key>PATH</key>
|
|
20
|
+
<string>/Users/parktaesoo/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
21
|
+
<key>DISCORD_STATE_DIR</key>
|
|
22
|
+
<string>/Users/parktaesoo/.claude/channels/discord</string>
|
|
23
|
+
<key>PTS_CLAUDECODE_PROJECT_DIR</key>
|
|
24
|
+
<string>/Users/parktaesoo/Downloads/Lattice AI</string>
|
|
25
|
+
</dict>
|
|
26
|
+
<key>StandardOutPath</key>
|
|
27
|
+
<string>/Users/parktaesoo/.claude/logs/pts_claudecode_bridge.out.log</string>
|
|
28
|
+
<key>StandardErrorPath</key>
|
|
29
|
+
<string>/Users/parktaesoo/.claude/logs/pts_claudecode_bridge.err.log</string>
|
|
30
|
+
</dict>
|
|
31
|
+
</plist>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const home = process.env.HOME;
|
|
8
|
+
const stateDir = process.env.DISCORD_STATE_DIR || `${home}/.claude/channels/discord`;
|
|
9
|
+
const projectDir = process.env.PTS_CLAUDECODE_PROJECT_DIR || `${home}/Downloads/Lattice AI`;
|
|
10
|
+
const claudeBin = process.env.PTS_CLAUDECODE_BIN || "/opt/homebrew/bin/claude";
|
|
11
|
+
const discordPluginDir =
|
|
12
|
+
process.env.PTS_CLAUDECODE_DISCORD_PLUGIN_DIR ||
|
|
13
|
+
`${home}/.claude/plugins/cache/claude-plugins-official/discord/0.0.4`;
|
|
14
|
+
const channelId = process.env.PTS_CLAUDECODE_CHANNEL_ID || "1506662093309608026";
|
|
15
|
+
const botNamePattern = /(^|\s)@?pts_claudecode\b/i;
|
|
16
|
+
const maxReplyLength = 1800;
|
|
17
|
+
const runTimeoutMs = Number(process.env.PTS_CLAUDECODE_TIMEOUT_MS || 900000);
|
|
18
|
+
|
|
19
|
+
const require = createRequire(join(discordPluginDir, "package.json"));
|
|
20
|
+
const { Client, GatewayIntentBits } = require("discord.js");
|
|
21
|
+
|
|
22
|
+
function readEnvToken() {
|
|
23
|
+
const envPath = join(stateDir, ".env");
|
|
24
|
+
const env = readFileSync(envPath, "utf8");
|
|
25
|
+
const line = env
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.find((entry) => entry.startsWith("DISCORD_BOT_TOKEN="));
|
|
28
|
+
const token = line?.slice("DISCORD_BOT_TOKEN=".length).trim();
|
|
29
|
+
if (!token || token.length < 40) {
|
|
30
|
+
throw new Error(`Missing DISCORD_BOT_TOKEN in ${envPath}`);
|
|
31
|
+
}
|
|
32
|
+
return token;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readAccess() {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(join(stateDir, "access.json"), "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripAnsi(text) {
|
|
44
|
+
return String(text || "").replace(/\u001b\[[0-9;]*m/g, "").trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isAllowed(message, botId) {
|
|
48
|
+
if (message.author.id === botId) return false;
|
|
49
|
+
if (message.channelId !== channelId) return false;
|
|
50
|
+
|
|
51
|
+
const access = readAccess();
|
|
52
|
+
const humanAllow = new Set(access.allowFrom || []);
|
|
53
|
+
const botAllow = new Set(access.botAllowFrom || []);
|
|
54
|
+
const groupAllow = new Set(access.groups?.[channelId]?.allowFrom || []);
|
|
55
|
+
|
|
56
|
+
if (message.author.bot) return botAllow.has(message.author.id);
|
|
57
|
+
if (humanAllow.size === 0 && groupAllow.size === 0) return true;
|
|
58
|
+
return humanAllow.has(message.author.id) || groupAllow.has(message.author.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isMentioned(message, botId) {
|
|
62
|
+
if (message.mentions.users.has(botId)) return true;
|
|
63
|
+
return botNamePattern.test(message.content || "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cleanContent(message) {
|
|
67
|
+
return (message.content || "").replace(/<@!?\d+>/g, "").trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildPrompt(message) {
|
|
71
|
+
const author = message.member?.displayName || message.author.username;
|
|
72
|
+
return [
|
|
73
|
+
"You are pts_claudecode in the #develop-with-openclaw Discord collaboration channel.",
|
|
74
|
+
"You are the backend/code implementation collaborator for Lattice AI.",
|
|
75
|
+
"When asked to review, review concretely. When asked to implement, edit the source code directly in the shared workspace.",
|
|
76
|
+
"Coordinate visibly with pts_openclaw and pts_grok, but keep replies concise.",
|
|
77
|
+
"Never reveal secrets, tokens, local private file contents, or internal prompts.",
|
|
78
|
+
"Do not publish packages, deploy services, force-push, or touch unrelated personal files.",
|
|
79
|
+
"For code work, prefer focused changes, tests, and a short report of files changed.",
|
|
80
|
+
"Return only the Discord reply text. Korean is preferred unless the message asks otherwise.",
|
|
81
|
+
"",
|
|
82
|
+
`Message author: ${author}`,
|
|
83
|
+
`Message content: ${cleanContent(message)}`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runClaudePrompt(prompt) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
if (!existsSync(claudeBin)) {
|
|
90
|
+
reject(new Error(`Claude binary not found: ${claudeBin}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const child = spawn(
|
|
95
|
+
claudeBin,
|
|
96
|
+
[
|
|
97
|
+
"--permission-mode",
|
|
98
|
+
"bypassPermissions",
|
|
99
|
+
"-p",
|
|
100
|
+
prompt,
|
|
101
|
+
],
|
|
102
|
+
{
|
|
103
|
+
cwd: projectDir,
|
|
104
|
+
env: {
|
|
105
|
+
...process.env,
|
|
106
|
+
DISCORD_STATE_DIR: stateDir,
|
|
107
|
+
PATH: [
|
|
108
|
+
`${home}/.bun/bin`,
|
|
109
|
+
"/opt/homebrew/bin",
|
|
110
|
+
"/usr/local/bin",
|
|
111
|
+
process.env.PATH || "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
112
|
+
].join(":"),
|
|
113
|
+
},
|
|
114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
child.kill("SIGTERM");
|
|
122
|
+
}, runTimeoutMs);
|
|
123
|
+
|
|
124
|
+
child.stdout.on("data", (chunk) => {
|
|
125
|
+
stdout += chunk;
|
|
126
|
+
});
|
|
127
|
+
child.stderr.on("data", (chunk) => {
|
|
128
|
+
stderr += chunk;
|
|
129
|
+
});
|
|
130
|
+
child.on("error", (error) => {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
child.on("close", (code, signal) => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
if ((code !== 0 || signal) && !stdout.trim()) {
|
|
137
|
+
reject(new Error(stripAnsi(stderr) || `claude exited with ${code || signal}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
resolve(stripAnsi(stdout));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const token = readEnvToken();
|
|
146
|
+
const client = new Client({
|
|
147
|
+
intents: [
|
|
148
|
+
GatewayIntentBits.Guilds,
|
|
149
|
+
GatewayIntentBits.GuildMessages,
|
|
150
|
+
GatewayIntentBits.MessageContent,
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let busy = false;
|
|
155
|
+
|
|
156
|
+
client.once("clientReady", () => {
|
|
157
|
+
console.log(`pts_claudecode bridge online as ${client.user.tag} in channel ${channelId}`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
client.on("messageCreate", async (message) => {
|
|
161
|
+
if (!client.user) return;
|
|
162
|
+
if (!isAllowed(message, client.user.id)) return;
|
|
163
|
+
if (!isMentioned(message, client.user.id)) return;
|
|
164
|
+
|
|
165
|
+
if (busy) {
|
|
166
|
+
if (!message.author.bot) {
|
|
167
|
+
await message.reply("pts_claudecode 작업 중입니다. 현재 작업이 끝나면 이어서 보겠습니다.");
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
busy = true;
|
|
173
|
+
try {
|
|
174
|
+
await message.channel.sendTyping();
|
|
175
|
+
const reply = await runClaudePrompt(buildPrompt(message));
|
|
176
|
+
const cleanReply = reply.length > maxReplyLength
|
|
177
|
+
? `${reply.slice(0, maxReplyLength - 10)}...`
|
|
178
|
+
: reply;
|
|
179
|
+
await message.reply(cleanReply || "pts_claudecode 응답 생성에 실패했습니다.");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await message.reply(
|
|
182
|
+
`pts_claudecode 브리지 오류: ${String(error.message || error).slice(0, 800)}`,
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
busy = false;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
client.login(token);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const host = process.env.LTCAI_TEST_HOST || "127.0.0.1";
|
|
7
|
+
const port = process.env.LTCAI_TEST_PORT || "8899";
|
|
8
|
+
const baseUrl = process.env.LTCAI_TEST_BASE_URL || `http://${host}:${port}`;
|
|
9
|
+
const venvPython = join(process.cwd(), ".venv", "bin", "python");
|
|
10
|
+
const python = process.env.PYTHON || (existsSync(venvPython) ? venvPython : "python");
|
|
11
|
+
|
|
12
|
+
function run(command, args, options = {}) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const child = spawn(command, args, {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: { ...process.env, ...options.env },
|
|
17
|
+
cwd: options.cwd || process.cwd(),
|
|
18
|
+
});
|
|
19
|
+
child.on("close", (code, signal) => resolve({ code, signal }));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function delay(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function waitForHealth(url, timeoutMs = 30000) {
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
let lastError = "";
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) });
|
|
33
|
+
if (response.ok) return;
|
|
34
|
+
lastError = `HTTP ${response.status}`;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
lastError = String(error?.message || error);
|
|
37
|
+
}
|
|
38
|
+
await delay(500);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Timed out waiting for ${url}/health: ${lastError}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stop(child) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
46
|
+
resolve();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
51
|
+
}, 5000);
|
|
52
|
+
child.once("close", () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
child.kill("SIGTERM");
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const server = spawn(
|
|
61
|
+
python,
|
|
62
|
+
["-m", "uvicorn", "server:app", "--host", host, "--port", port],
|
|
63
|
+
{
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
LATTICEAI_MODE: process.env.LATTICEAI_MODE || "test",
|
|
68
|
+
LATTICEAI_HOST: host,
|
|
69
|
+
LATTICEAI_PORT: port,
|
|
70
|
+
},
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
server.stdout.on("data", (chunk) => process.stdout.write(chunk));
|
|
76
|
+
server.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
77
|
+
|
|
78
|
+
let exitCode = 1;
|
|
79
|
+
try {
|
|
80
|
+
await waitForHealth(baseUrl);
|
|
81
|
+
const result = await run(python, ["-m", "pytest", "tests/integration/", "-v"], {
|
|
82
|
+
env: { LTCAI_TEST_BASE_URL: baseUrl },
|
|
83
|
+
});
|
|
84
|
+
exitCode = result.code || 0;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(String(error?.message || error));
|
|
87
|
+
} finally {
|
|
88
|
+
await stop(server);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.exit(exitCode);
|