ltcai 12.4.0 → 12.5.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.
Files changed (82) hide show
  1. package/README.md +23 -37
  2. package/docs/CHANGELOG.md +13 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +9 -9
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/TRUST_MODEL.md +1 -1
  8. package/docs/WHY_LATTICE.md +1 -1
  9. package/docs/kg-schema.md +1 -1
  10. package/lattice_brain/__init__.py +1 -1
  11. package/latticeai/__init__.py +1 -1
  12. package/latticeai/api/models.py +21 -1
  13. package/latticeai/api/search.py +8 -1
  14. package/latticeai/api/worker_compute.py +9 -1
  15. package/latticeai/core/embedding_providers/__init__.py +14 -0
  16. package/latticeai/core/embedding_providers/autodetect.py +91 -24
  17. package/latticeai/core/embedding_providers/base.py +10 -2
  18. package/latticeai/core/embedding_providers/profiles.py +44 -0
  19. package/latticeai/core/embedding_providers/text.py +325 -2
  20. package/latticeai/services/architecture_readiness.py +1 -1
  21. package/latticeai/services/embedding_recommendation.py +229 -0
  22. package/latticeai/services/model_capability_registry.py +37 -0
  23. package/latticeai/services/model_catalog.py +130 -0
  24. package/latticeai/services/product_readiness.py +1 -1
  25. package/package.json +1 -1
  26. package/scripts/bump_version.py +106 -6
  27. package/scripts/check_current_release_docs.mjs +1 -1
  28. package/scripts/check_screenshot_pixel_delta.py +6 -2
  29. package/src-tauri/Cargo.lock +10 -10
  30. package/src-tauri/Cargo.toml +1 -1
  31. package/src-tauri/tauri.conf.json +1 -1
  32. package/static/app/asset-manifest.json +44 -44
  33. package/static/app/assets/{Act-inLiZfDE.js → Act-DQ2Ldwo8.js} +2 -2
  34. package/static/app/assets/{AdminConsole-iLFwSJSg.js → AdminConsole-y6EjiXeH.js} +1 -1
  35. package/static/app/assets/{Brain-Cb7noiNc.js → Brain-CRWWw-h9.js} +2 -2
  36. package/static/app/assets/{BrainHome-CfiJ4tau.js → BrainHome-cmvTu7Ea.js} +1 -1
  37. package/static/app/assets/{BrainSignals-ShxKNNqr.js → BrainSignals-gaNn5E4-.js} +1 -1
  38. package/static/app/assets/{Capture-Cajj1Trf.js → Capture-BJK7in1j.js} +1 -1
  39. package/static/app/assets/{Chronicle-CPd8GP_O.js → Chronicle-BuYZ9lMt.js} +1 -1
  40. package/static/app/assets/{CommandPalette-Cb_GWoYC.js → CommandPalette-BjFwuKaA.js} +1 -1
  41. package/static/app/assets/{DigitalBrainExplorer-Df6KVrBz.js → DigitalBrainExplorer-DTBRPYJ3.js} +1 -1
  42. package/static/app/assets/Library-BsmZ-YVh.js +1 -0
  43. package/static/app/assets/{LivingBrain-u9X_XGC2.js → LivingBrain-aT9hgkPV.js} +1 -1
  44. package/static/app/assets/ProductFlow-Ct0QLLY5.js +1 -0
  45. package/static/app/assets/{ReviewCard-C64YFHOR.js → ReviewCard-CdooYtWr.js} +1 -1
  46. package/static/app/assets/{RunsListPanel-DKZEJcxF.js → RunsListPanel-Btf2DIrG.js} +1 -1
  47. package/static/app/assets/{System-Dretrknf.js → System-B77M4t-M.js} +1 -1
  48. package/static/app/assets/{WorkflowGraph-CE6XgWjJ.js → WorkflowGraph-u3ZKLvZe.js} +1 -1
  49. package/static/app/assets/{WorkflowsPanel-MKU05J7_.js → WorkflowsPanel--nTFq3Cs.js} +2 -2
  50. package/static/app/assets/{actHelpers-BjMnKMCI.js → actHelpers-xNnCfUeC.js} +1 -1
  51. package/static/app/assets/arrow-left-Cp4NHbfX.js +1 -0
  52. package/static/app/assets/{bot-Qgvc2PGU.js → bot-CXwvpODC.js} +1 -1
  53. package/static/app/assets/{brain-BwGnubn8.js → brain-BzjoGgor.js} +1 -1
  54. package/static/app/assets/{button-DEloiIsj.js → button-_hHOhp7g.js} +1 -1
  55. package/static/app/assets/circle-check-qG5dMahn.js +1 -0
  56. package/static/app/assets/{circle-pause-CkkmXxA9.js → circle-pause-DBOqYYMh.js} +1 -1
  57. package/static/app/assets/{circle-play-rnb6A1zq.js → circle-play-DA0Vf2NH.js} +1 -1
  58. package/static/app/assets/{cpu-CzcGFQQq.js → cpu-DwsE0_o3.js} +1 -1
  59. package/static/app/assets/{download-BTQigJvp.js → download-iCoe2f2Q.js} +1 -1
  60. package/static/app/assets/{folder-open-CIXPPhhm.js → folder-open-C1vNOMhy.js} +1 -1
  61. package/static/app/assets/{hard-drive-CZXxdIcT.js → hard-drive-a73KaYth.js} +1 -1
  62. package/static/app/assets/index-Dbb2YhYw.css +2 -0
  63. package/static/app/assets/{index-CHQbWj9a.js → index-DpuEeYmP.js} +2 -2
  64. package/static/app/assets/{input-CTZHuOXe.js → input-BclQNHyj.js} +1 -1
  65. package/static/app/assets/{link-2-BsG1NQUA.js → link-2-D23wihBe.js} +1 -1
  66. package/static/app/assets/{permissionCopy-klyWL3ai.js → permissionCopy-DiGZRKAY.js} +1 -1
  67. package/static/app/assets/{primitives-ZmN4jzq-.js → primitives-1S-XCgmz.js} +1 -1
  68. package/static/app/assets/search-Cl2k5uhD.js +1 -0
  69. package/static/app/assets/{share-2-Cuv9rj7A.js → share-2-47_av_aF.js} +1 -1
  70. package/static/app/assets/{shield-alert-KKySFd03.js → shield-alert-BvKq2P-G.js} +1 -1
  71. package/static/app/assets/{textarea-CBwqgGSA.js → textarea-DcLO52bu.js} +1 -1
  72. package/static/app/assets/{useFocusTrap-D93vds_Z.js → useFocusTrap-CpqVPzJ_.js} +1 -1
  73. package/static/app/assets/{useMutation-B9hHPD0p.js → useMutation-DhI3jqCN.js} +1 -1
  74. package/static/app/assets/workspace-DwNHbqaX.js +1 -0
  75. package/static/app/index.html +2 -2
  76. package/static/app/assets/Library-Dk5Mefl_.js +0 -1
  77. package/static/app/assets/ProductFlow-CpX0BMr9.js +0 -1
  78. package/static/app/assets/arrow-left-Cbck4Ig2.js +0 -1
  79. package/static/app/assets/circle-check-CDygnRMn.js +0 -1
  80. package/static/app/assets/index-C_dThdD6.css +0 -2
  81. package/static/app/assets/search-xjKVb4Nh.js +0 -1
  82. package/static/app/assets/workspace-JGzrc0D8.js +0 -1
@@ -0,0 +1,229 @@
1
+ """Embedding model capability registry and PC-tailored hardware recommendations.
2
+
3
+ Dedicated module to keep `model_capability_registry.py` under the 1000-line cap
4
+ while providing first-class Google EmbeddingGemma 2 / 300M recommendation logic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Callable, Dict, List, Optional
10
+
11
+
12
+ def build_embedding_capabilities(
13
+ model_capability_cls: type,
14
+ hardware_profile_cls: type,
15
+ verification_status_cls: type,
16
+ ) -> List[Any]:
17
+ """Construct ModelCapability instances for EmbeddingGemma 2 / 300M."""
18
+ return [
19
+ model_capability_cls(
20
+ id="google/embeddinggemma-2",
21
+ hf_repo_id="google/embeddinggemma-2",
22
+ name="Google EmbeddingGemma 2",
23
+ family="EmbeddingGemma",
24
+ tag="local-embedding",
25
+ size="1.5GB",
26
+ modality="text",
27
+ architecture="gemma3",
28
+ download_size_gb=1.48,
29
+ quantization="FP16",
30
+ provider_hints=["local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"],
31
+ download_strategy="hf_hub",
32
+ load_strategy="sentence_transformers",
33
+ hardware=hardware_profile_cls(
34
+ min_ram_gb=4.0, recommended_ram_gb=8.0, apple_silicon_pref=True, cuda_pref=True,
35
+ notes="Google DeepMind 740M multilingual & code semantic embedding model (768d). Uses ~1.5GB in FP16 HF or ~0.5GB in 4-bit MLX.",
36
+ ),
37
+ source_country="미국", source_company="Google",
38
+ verification=verification_status_cls(
39
+ hf_exists=True, hf_last_checked="2026-11-11", has_config=True, has_tokenizer=True,
40
+ has_weights_hint=True, pipeline_tag="feature-extraction", likes=120, license="apache-2.0",
41
+ notes="Hugging Face google/embeddinggemma-2, 768d semantic embeddings.", verified_by="hf-api-light",
42
+ ),
43
+ recommended_default=True,
44
+ display_priority=70,
45
+ category="embedding",
46
+ task="embedding",
47
+ dimensions=768,
48
+ ),
49
+ model_capability_cls(
50
+ id="mlx-community/embeddinggemma-2-4bit",
51
+ hf_repo_id="mlx-community/embeddinggemma-2-4bit",
52
+ name="Google EmbeddingGemma 2 4-bit",
53
+ family="EmbeddingGemma",
54
+ tag="local-embedding",
55
+ size="0.4GB",
56
+ modality="text",
57
+ architecture="gemma3",
58
+ download_size_gb=0.42,
59
+ quantization="4bit",
60
+ provider_hints=["local_mlx"],
61
+ download_strategy="hf_hub",
62
+ load_strategy="mlx_embeddings",
63
+ hardware=hardware_profile_cls(
64
+ min_ram_gb=4.0, recommended_ram_gb=8.0, apple_silicon_pref=True,
65
+ notes="Apple Silicon MLX 4-bit quantized EmbeddingGemma 2 (768d, 0.42GB on disk, ~0.5GB RAM).",
66
+ ),
67
+ source_country="미국", source_company="Google",
68
+ verification=verification_status_cls(
69
+ hf_exists=True, hf_last_checked="2026-11-11", has_config=True, has_tokenizer=True,
70
+ has_weights_hint=True, pipeline_tag="feature-extraction", likes=45, license="apache-2.0",
71
+ notes="library_name=mlx, ungated, 4-bit quantized EmbeddingGemma 2.", verified_by="hf-api-light",
72
+ ),
73
+ recommended_default=True,
74
+ display_priority=71,
75
+ category="embedding",
76
+ task="embedding",
77
+ dimensions=768,
78
+ ),
79
+ model_capability_cls(
80
+ id="google/embeddinggemma-300m",
81
+ hf_repo_id="google/embeddinggemma-300m",
82
+ name="Google EmbeddingGemma 300M",
83
+ family="EmbeddingGemma",
84
+ tag="local-embedding",
85
+ size="0.6GB",
86
+ modality="text",
87
+ architecture="gemma3",
88
+ download_size_gb=0.60,
89
+ quantization="FP16",
90
+ provider_hints=["local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"],
91
+ download_strategy="hf_hub",
92
+ load_strategy="sentence_transformers",
93
+ hardware=hardware_profile_cls(
94
+ min_ram_gb=2.0, recommended_ram_gb=4.0, apple_silicon_pref=True, cuda_pref=True,
95
+ notes="Compact 300M multilingual semantic embedding model (768d) for constrained-RAM PCs (<8GB).",
96
+ ),
97
+ source_country="미국", source_company="Google",
98
+ verification=verification_status_cls(
99
+ hf_exists=True, hf_last_checked="2026-11-11", has_config=True, has_tokenizer=True,
100
+ has_weights_hint=True, pipeline_tag="feature-extraction", likes=85, license="apache-2.0",
101
+ notes="Hugging Face google/embeddinggemma-300m, 768d semantic embeddings.", verified_by="hf-api-light",
102
+ ),
103
+ recommended_default=False,
104
+ display_priority=72,
105
+ category="embedding",
106
+ task="embedding",
107
+ dimensions=768,
108
+ ),
109
+ model_capability_cls(
110
+ id="mlx-community/embeddinggemma-300m-4bit",
111
+ hf_repo_id="mlx-community/embeddinggemma-300m-4bit",
112
+ name="Google EmbeddingGemma 300M 4-bit",
113
+ family="EmbeddingGemma",
114
+ tag="local-embedding",
115
+ size="0.3GB",
116
+ modality="text",
117
+ architecture="gemma3",
118
+ download_size_gb=0.30,
119
+ quantization="4bit",
120
+ provider_hints=["local_mlx"],
121
+ download_strategy="hf_hub",
122
+ load_strategy="mlx_embeddings",
123
+ hardware=hardware_profile_cls(
124
+ min_ram_gb=2.0, recommended_ram_gb=4.0, apple_silicon_pref=True,
125
+ notes="Ultralight 4-bit MLX EmbeddingGemma 300M (~0.3GB RAM) for low-memory Macs.",
126
+ ),
127
+ source_country="미국", source_company="Google",
128
+ verification=verification_status_cls(
129
+ hf_exists=True, hf_last_checked="2026-11-11", has_config=True, has_tokenizer=True,
130
+ has_weights_hint=True, pipeline_tag="feature-extraction", likes=30, license="apache-2.0",
131
+ notes="library_name=mlx, ungated, 4-bit quantized EmbeddingGemma 300M.", verified_by="hf-api-light",
132
+ ),
133
+ recommended_default=False,
134
+ display_priority=73,
135
+ category="embedding",
136
+ task="embedding",
137
+ dimensions=768,
138
+ ),
139
+ ]
140
+
141
+
142
+ def recommend_embedding_for_hardware(
143
+ *,
144
+ ram_gb: Optional[float] = None,
145
+ apple_silicon: bool = False,
146
+ cuda: bool = False,
147
+ get_capability_fn: Optional[Callable[[str], Any]] = None,
148
+ ) -> Dict[str, Any]:
149
+ """Return a PC-tailored embedding model recommendation accounting for RAM & accelerator."""
150
+ effective_ram = 16.0 if ram_gb is None or ram_gb <= 0 else float(ram_gb)
151
+ cap = get_capability_fn("google/embeddinggemma-2") if get_capability_fn else None
152
+ mlx_cap = get_capability_fn("mlx-community/embeddinggemma-2-4bit") if get_capability_fn else None
153
+
154
+ if effective_ram >= 8.0:
155
+ use_mlx = bool(apple_silicon and mlx_cap is not None)
156
+ footprint_gb = 0.5 if use_mlx else 1.5
157
+ chosen_cap = cap or mlx_cap
158
+ base = chosen_cap.to_legacy_dict() if chosen_cap is not None else {}
159
+ return {
160
+ **base,
161
+ "id": "google/embeddinggemma-2",
162
+ "model_id": "google/embeddinggemma-2",
163
+ "hf_repo_id": "google/embeddinggemma-2",
164
+ "mlx_repo_id": "mlx-community/embeddinggemma-2-4bit",
165
+ "recommended_load_id": "mlx-community/embeddinggemma-2-4bit" if use_mlx else "google/embeddinggemma-2",
166
+ "recommended_engine": "local_mlx" if use_mlx else "huggingface",
167
+ "provider": "mlx" if use_mlx else "huggingface",
168
+ "profile_id": "mlx:embeddinggemma-2" if use_mlx else "local:embeddinggemma-2",
169
+ "name": "Google EmbeddingGemma 2",
170
+ "display_name": "Google EmbeddingGemma 2",
171
+ "family": "EmbeddingGemma",
172
+ "category": "embedding",
173
+ "role": "embedding",
174
+ "task": "embedding",
175
+ "tag": "local-embedding",
176
+ "modality": "text",
177
+ "dimensions": 768,
178
+ "size": "0.4GB" if use_mlx else "1.5GB",
179
+ "download_size": "0.4GB" if use_mlx else "1.5GB",
180
+ "download_size_gb": 0.42 if use_mlx else 1.48,
181
+ "memory_footprint_gb": footprint_gb,
182
+ "effective_llm_ram_gb": max(0.0, round(effective_ram - footprint_gb, 2)),
183
+ "status": "recommended",
184
+ "load_status": "ready",
185
+ "supported": True,
186
+ "recommended_default": True,
187
+ "display_priority": 70,
188
+ "reason": (
189
+ f"Apple Silicon ({int(round(effective_ram))}GB RAM) 맞춤 4-bit MLX / Hugging Face 시맨틱 임베딩 모델 (Google EmbeddingGemma 2, 768차원)"
190
+ if apple_silicon
191
+ else f"{int(round(effective_ram))}GB RAM PC 맞춤 Hugging Face 시맨틱 임베딩 모델 (Google EmbeddingGemma 2, 768차원)"
192
+ ),
193
+ }
194
+
195
+ cap_300m = get_capability_fn("google/embeddinggemma-300m") if get_capability_fn else None
196
+ use_mlx = bool(apple_silicon)
197
+ footprint_gb = 0.3
198
+ base = cap_300m.to_legacy_dict() if cap_300m else {}
199
+ return {
200
+ **base,
201
+ "id": "google/embeddinggemma-300m",
202
+ "model_id": "google/embeddinggemma-300m",
203
+ "hf_repo_id": "google/embeddinggemma-300m",
204
+ "mlx_repo_id": "mlx-community/embeddinggemma-300m-4bit",
205
+ "recommended_load_id": "mlx-community/embeddinggemma-300m-4bit" if use_mlx else "google/embeddinggemma-300m",
206
+ "recommended_engine": "local_mlx" if use_mlx else "huggingface",
207
+ "provider": "mlx" if use_mlx else "huggingface",
208
+ "profile_id": "local:embeddinggemma-300m",
209
+ "name": "Google EmbeddingGemma 300M",
210
+ "display_name": "Google EmbeddingGemma 300M",
211
+ "family": "EmbeddingGemma",
212
+ "category": "embedding",
213
+ "role": "embedding",
214
+ "task": "embedding",
215
+ "tag": "local-embedding",
216
+ "modality": "text",
217
+ "dimensions": 768,
218
+ "size": "0.3GB" if use_mlx else "0.6GB",
219
+ "download_size": "0.3GB" if use_mlx else "0.6GB",
220
+ "download_size_gb": 0.30 if use_mlx else 0.60,
221
+ "memory_footprint_gb": footprint_gb,
222
+ "effective_llm_ram_gb": max(0.0, round(effective_ram - footprint_gb, 2)),
223
+ "status": "recommended",
224
+ "load_status": "ready",
225
+ "supported": True,
226
+ "recommended_default": True,
227
+ "display_priority": 72,
228
+ "reason": f"저사양 PC ({int(round(effective_ram))}GB RAM) 맞춤 초경량 시맨틱 임베딩 모델 (Google EmbeddingGemma 300M, 768차원)",
229
+ }
@@ -43,6 +43,13 @@ from __future__ import annotations
43
43
  from dataclasses import dataclass, field
44
44
  from typing import Any, Dict, List, Optional
45
45
 
46
+ from .embedding_recommendation import (
47
+ build_embedding_capabilities,
48
+ )
49
+ from .embedding_recommendation import (
50
+ recommend_embedding_for_hardware as _rec_embedding_for_hardware,
51
+ )
52
+
46
53
 
47
54
  @dataclass(frozen=True)
48
55
  class HardwareProfile:
@@ -109,6 +116,9 @@ class ModelCapability:
109
116
  # UI / rec hints
110
117
  recommended_default: bool = False
111
118
  display_priority: int = 100
119
+ category: str = "chat"
120
+ task: str = "chat"
121
+ dimensions: Optional[int] = None
112
122
 
113
123
  def to_legacy_dict(self) -> Dict[str, Any]:
114
124
  """Exact shape expected by older ENGINE_MODEL_CATALOG consumers + extra 5.2 fields."""
@@ -123,6 +133,9 @@ class ModelCapability:
123
133
  "modality": self.modality,
124
134
  "architecture": self.architecture,
125
135
  "lifecycle": self.lifecycle,
136
+ "category": self.category,
137
+ "task": self.task,
138
+ "role": "embedding" if self.category == "embedding" else "chat",
126
139
  "source_country": self.source_country,
127
140
  "source_company": self.source_company,
128
141
  "execution_method": self.execution_method,
@@ -162,6 +175,8 @@ class ModelCapability:
162
175
  },
163
176
  "recommended_default": self.recommended_default,
164
177
  }
178
+ if self.dimensions is not None:
179
+ base["dimensions"] = self.dimensions
165
180
  return base
166
181
 
167
182
 
@@ -526,6 +541,7 @@ _REGISTRY: List[ModelCapability] = [
526
541
  display_priority=60,
527
542
  ),
528
543
  ]
544
+ _REGISTRY.extend(build_embedding_capabilities(ModelCapability, HardwareProfile, VerificationStatus))
529
545
 
530
546
 
531
547
  # ── Recognised-only entries (LEGACY) ──────────────────────────────────────────
@@ -783,3 +799,24 @@ def get_verified_models() -> List[Dict[str, Any]]:
783
799
  if c.verification.hf_exists and c.verification.has_config and c.verification.has_tokenizer
784
800
  ]
785
801
 
802
+
803
+ def get_embedding_capabilities() -> List[ModelCapability]:
804
+ """Return recommended embedding model capabilities (Google EmbeddingGemma 2 / 300M)."""
805
+ return [c for c in _REGISTRY if c.category == "embedding"]
806
+
807
+
808
+ def recommend_embedding_for_hardware(
809
+ *,
810
+ ram_gb: Optional[float] = None,
811
+ apple_silicon: bool = False,
812
+ cuda: bool = False,
813
+ ) -> Dict[str, Any]:
814
+ """Return a PC-tailored embedding model recommendation accounting for RAM & accelerator."""
815
+ return _rec_embedding_for_hardware(
816
+ ram_gb=ram_gb,
817
+ apple_silicon=apple_silicon,
818
+ cuda=cuda,
819
+ get_capability_fn=get_capability,
820
+ )
821
+
822
+
@@ -26,9 +26,15 @@ from latticeai.services.model_capability_registry import (
26
26
  from latticeai.services.model_capability_registry import (
27
27
  get_capability as _get_capability,
28
28
  )
29
+ from latticeai.services.model_capability_registry import (
30
+ get_embedding_capabilities as _get_embedding_capabilities,
31
+ )
29
32
  from latticeai.services.model_capability_registry import (
30
33
  get_verified_models as _get_verified_models,
31
34
  )
35
+ from latticeai.services.model_capability_registry import (
36
+ recommend_embedding_for_hardware as _recommend_embedding_for_hardware,
37
+ )
32
38
 
33
39
  ENGINE_INSTALLERS: Dict[str, Dict[str, Any]] = {
34
40
  "local_mlx": {
@@ -170,6 +176,18 @@ _ENGINE_ROUTES: Dict[str, Dict[str, str]] = {
170
176
  "mlx-community/Qwen3.6-35B-A3B-4bit": _gguf_routes(
171
177
  "mlx-community/Qwen3.6-35B-A3B-4bit", "ggml-org/Qwen3.6-35B-A3B-GGUF", "Qwen/Qwen3.6-35B-A3B",
172
178
  ),
179
+ "google/embeddinggemma-2": _gguf_routes(
180
+ "google/embeddinggemma-2", "ggml-org/embeddinggemma-2-GGUF", "google/embeddinggemma-2",
181
+ ),
182
+ "mlx-community/embeddinggemma-2-4bit": _gguf_routes(
183
+ "mlx-community/embeddinggemma-2-4bit", "mlx-community/embeddinggemma-2-4bit-GGUF", "mlx-community/embeddinggemma-2-4bit",
184
+ ),
185
+ "google/embeddinggemma-300m": _gguf_routes(
186
+ "google/embeddinggemma-300m", "ggml-org/embeddinggemma-300m-GGUF", "google/embeddinggemma-300m",
187
+ ),
188
+ "mlx-community/embeddinggemma-300m-4bit": _gguf_routes(
189
+ "mlx-community/embeddinggemma-300m-4bit", "mlx-community/embeddinggemma-300m-4bit-GGUF", "mlx-community/embeddinggemma-300m-4bit",
190
+ ),
173
191
  }
174
192
 
175
193
  # Names people actually type that are not the repo's own short name. Kept
@@ -182,6 +200,8 @@ _COMMON_MISNAMES: Dict[str, str] = {
182
200
  "gemma-4-26b-optiq-4bit": "mlx-community/gemma-4-26B-A4B-it-qat-OptiQ-4bit",
183
201
  "qwen-3.8-27b-4bit": "mlx-community/Qwen3.8-27B-4bit",
184
202
  "qwen3.8-27b": "mlx-community/Qwen3.8-27B-4bit",
203
+ "embeddinggemma2": "google/embeddinggemma-2",
204
+ "embeddinggemma-2-hf": "google/embeddinggemma-2",
185
205
  }
186
206
 
187
207
  # Keyed by both the lowercase short name ("gemma-4-12b-it-4bit") and the
@@ -198,7 +218,9 @@ MODEL_ENGINE_ALIASES: Dict[str, Dict[str, str]] = {
198
218
  # Also expose registry helpers directly from here for consumers who want the rich objects
199
219
  get_all_capabilities = _get_all_capabilities
200
220
  get_capability = _get_capability
221
+ get_embedding_capabilities = _get_embedding_capabilities
201
222
  get_verified_models = _get_verified_models
223
+ recommend_embedding_for_hardware = _recommend_embedding_for_hardware
202
224
 
203
225
  _VERSIONED_MODEL_PATTERNS = (
204
226
  ("gemma", re.compile(r"\bgemma[-\s]?(\d+(?:\.\d+)?)", re.IGNORECASE)),
@@ -213,6 +235,8 @@ def _version_tuple(raw: str) -> tuple[int, ...]:
213
235
 
214
236
  def _model_family_version(model: Dict[str, Any]) -> Optional[tuple[str, tuple[int, ...]]]:
215
237
  text = " ".join(str(model.get(key) or "") for key in ("family", "name", "id"))
238
+ if "embeddinggemma" in text.lower() or model.get("category") == "embedding" or model.get("task") == "embedding":
239
+ return None
216
240
  for family, pattern in _VERSIONED_MODEL_PATTERNS:
217
241
  match = pattern.search(text)
218
242
  if match:
@@ -270,6 +294,8 @@ _BLOCKED_CATALOG_FRAGMENTS = (
270
294
 
271
295
  def _is_blocked_catalog_id(model: Dict[str, Any]) -> bool:
272
296
  ident = str(model.get("id") or "").lower()
297
+ if "embeddinggemma" in ident:
298
+ return False
273
299
  return any(fragment in ident for fragment in _BLOCKED_CATALOG_FRAGMENTS)
274
300
 
275
301
 
@@ -311,3 +337,107 @@ def _finalize_engine_catalog(
311
337
 
312
338
 
313
339
  ENGINE_MODEL_CATALOG = _finalize_engine_catalog(_RAW_ENGINE_MODEL_CATALOG)
340
+
341
+
342
+ def recommend_models_for_pc(
343
+ *,
344
+ ram_gb: float = 16.0,
345
+ apple_silicon: bool = True,
346
+ cuda: bool = False,
347
+ engine: str = "local_mlx",
348
+ ) -> Dict[str, Any]:
349
+ """Compute PC-tailored model recommendations including EmbeddingGemma 2.
350
+
351
+ Evaluates the system RAM/VRAM budget while accounting for the embedding
352
+ model's memory footprint (``google/embeddinggemma-2`` uses ~0.5 GB in 4-bit
353
+ MLX / ~1.5 GB in FP16 HF; ``google/embeddinggemma-300m`` uses ~0.3 GB) so
354
+ the recommended LLM + embedding model setup fits comfortably on the host.
355
+ """
356
+ embedding_rec = recommend_embedding_for_hardware(
357
+ ram_gb=ram_gb,
358
+ apple_silicon=apple_silicon,
359
+ cuda=cuda,
360
+ )
361
+ embedding_ram_gb = float(embedding_rec.get("memory_footprint_gb") or 0.5)
362
+ effective_llm_ram_gb = max(0.0, float(ram_gb) - embedding_ram_gb)
363
+
364
+ catalog_rows = list(ENGINE_MODEL_CATALOG.get(engine) or ENGINE_MODEL_CATALOG.get("local_mlx") or [])
365
+ models_out: List[Dict[str, Any]] = []
366
+ chat_candidates: List[Dict[str, Any]] = []
367
+
368
+ for row in catalog_rows:
369
+ entry = dict(row)
370
+ category = str(entry.get("category") or "chat")
371
+ min_ram = float(entry.get("min_ram_gb") or 4.0)
372
+ rec_ram = float(entry.get("recommended_ram_gb") or max(min_ram, 8.0))
373
+ dl_gb = float(entry.get("download_size_gb") or 0.0)
374
+ if category == "embedding":
375
+ fits = float(ram_gb) + 0.01 >= min_ram
376
+ comfortable = float(ram_gb) + 0.01 >= rec_ram
377
+ status = "recommended" if comfortable else ("compatible" if fits else "not_recommended")
378
+ entry["compatibility"] = {
379
+ "status": status,
380
+ "reason": embedding_rec.get("reason") if status != "not_recommended" else f"최소 {min_ram:.0f}GB RAM이 필요합니다.",
381
+ "min_ram_gb": min_ram,
382
+ "recommended_ram_gb": rec_ram,
383
+ }
384
+ models_out.append(entry)
385
+ continue
386
+
387
+ # Account for the embedding model footprint when evaluating LLM fit.
388
+ exceeds_combined = dl_gb > 0.0 and (dl_gb + embedding_ram_gb) > float(ram_gb)
389
+ if exceeds_combined or effective_llm_ram_gb + 0.01 < min_ram:
390
+ status = "not_recommended"
391
+ reason = (
392
+ f"임베딩 모델({embedding_rec['id']}, ~{embedding_ram_gb:.1f}GB)과 함께 실행 시 "
393
+ f"최소 {min_ram + embedding_ram_gb:.1f}GB RAM이 필요합니다 (현재 {ram_gb:.1f}GB)."
394
+ )
395
+ elif effective_llm_ram_gb + 0.01 >= rec_ram or float(ram_gb) + 0.01 >= rec_ram:
396
+ status = "recommended"
397
+ reason = (
398
+ f"{ram_gb:.0f}GB RAM 환경에서 {embedding_rec['name']} (~{embedding_ram_gb:.1f}GB) 임베딩 모델과 "
399
+ f"동시에 여유 있게 실행됩니다 (권장 {rec_ram:.0f}GB)."
400
+ )
401
+ else:
402
+ status = "compatible"
403
+ reason = (
404
+ f"실행 가능하지만 임베딩 모델(~{embedding_ram_gb:.1f}GB) 병행 시 메모리가 빠듯할 수 있습니다 "
405
+ f"(최소 {min_ram:.0f}GB / 권장 {rec_ram:.0f}GB)."
406
+ )
407
+ entry["compatibility"] = {
408
+ "status": status,
409
+ "reason": reason,
410
+ "min_ram_gb": min_ram,
411
+ "recommended_ram_gb": rec_ram,
412
+ "embedding_reserved_ram_gb": embedding_ram_gb,
413
+ }
414
+ models_out.append(entry)
415
+ if status in {"recommended", "compatible"}:
416
+ chat_candidates.append(entry)
417
+
418
+ # Ensure the PC-tailored embedding recommendation is present in models_out
419
+ if not any(str(m.get("category") or "") == "embedding" for m in models_out):
420
+ models_out.append(dict(embedding_rec))
421
+
422
+ chat_candidates.sort(
423
+ key=lambda m: (
424
+ 0 if m.get("compatibility", {}).get("status") == "recommended" else 1,
425
+ -float(m.get("recommended_ram_gb") or 0.0),
426
+ 0 if m.get("recommended_default") else 1,
427
+ int(m.get("recommendation_priority") or 99),
428
+ )
429
+ )
430
+ top_pick = chat_candidates[0] if chat_candidates else None
431
+
432
+ return {
433
+ "ram_gb": float(ram_gb),
434
+ "effective_ram_gb": round(effective_llm_ram_gb, 2),
435
+ "embedding_reserved_ram_gb": embedding_ram_gb,
436
+ "apple_silicon": bool(apple_silicon),
437
+ "cuda": bool(cuda),
438
+ "top_pick": top_pick,
439
+ "embedding_model": embedding_rec["id"],
440
+ "embedding_recommendation": embedding_rec,
441
+ "models": models_out,
442
+ }
443
+
@@ -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.4.0"
31
+ PRODUCT_VERSION_TARGET = "12.5.0"
32
32
 
33
33
 
34
34
  @dataclass(frozen=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "12.4.0",
3
+ "version": "12.5.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": {
@@ -13,6 +13,7 @@ Usage:
13
13
  from __future__ import annotations
14
14
 
15
15
  import argparse
16
+ import hashlib
16
17
  import json
17
18
  import re
18
19
  import sys
@@ -44,6 +45,25 @@ RUST_CRATES = (
44
45
  )
45
46
  RUST_LOCKFILES = ("rust/Cargo.lock", "src-tauri/Cargo.lock")
46
47
 
48
+ # Committed OpenAPI fragments (`rust/fixtures/openapi/<family>.json`) plus
49
+ # `_envelope.json` and `_index.json`. Serialized with `sort_keys=True, indent=2`
50
+ # (`scripts/gen_openapi_fragments.py::canonical_json`).
51
+ OPENAPI_FRAGMENTS = (
52
+ "admin",
53
+ "auth",
54
+ "chat",
55
+ "knowledge_search",
56
+ "mcp_market",
57
+ "memory_brain",
58
+ "models_misc",
59
+ "portability_network",
60
+ "review_proposals",
61
+ "static_ui",
62
+ "ui_redirects",
63
+ "worker_keep",
64
+ "workspace",
65
+ )
66
+
47
67
  # (path, kind, pattern) — pattern groups: (prefix, version)
48
68
  TARGETS = [
49
69
  ("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
@@ -68,9 +88,20 @@ TARGETS = [
68
88
  for crate in RUST_CRATES
69
89
  ],
70
90
  ("static/app/asset-manifest.json", "json", "version"),
91
+ ("frontend/openapi.json", "openapi-spec", None),
92
+ ("rust/fixtures/openapi/_envelope.json", "openapi-envelope", None),
93
+ *[
94
+ (f"rust/fixtures/openapi/{family}.json", "openapi-fragment", None)
95
+ for family in OPENAPI_FRAGMENTS
96
+ ],
97
+ ("rust/fixtures/openapi/_index.json", "openapi-index", None),
71
98
  ]
72
99
 
73
100
 
101
+ def _canonical_openapi_json(payload: object) -> str:
102
+ return json.dumps(payload, sort_keys=True, indent=2) + "\n"
103
+
104
+
74
105
  def bump(version: str, *, check: bool = False) -> int:
75
106
  if not re.fullmatch(r"\d+\.\d+\.\d+([-.][0-9A-Za-z.]+)?", version):
76
107
  print(f"error: '{version}' is not a sane semantic version", file=sys.stderr)
@@ -82,26 +113,96 @@ def bump(version: str, *, check: bool = False) -> int:
82
113
  print(f" skip {rel} (missing)")
83
114
  continue
84
115
  text = path.read_text(encoding="utf-8")
116
+ current = ""
117
+ new = text
85
118
  if kind == "regex":
86
- new, n = re.subn(spec, lambda m: m.group(1) + version + m.group(3), text, count=1, flags=re.MULTILINE)
87
- changed = n == 1 and new != text
88
- ok = n == 1
119
+ m = re.search(spec, text, flags=re.MULTILINE)
120
+ ok = m is not None
121
+ if ok and m is not None:
122
+ current = m.group(2)
123
+ new, n = re.subn(spec, lambda match: match.group(1) + version + match.group(3), text, count=1, flags=re.MULTILINE)
124
+ changed = n == 1 and new != text
125
+ else:
126
+ changed = False
89
127
  elif kind == "json":
90
128
  data = json.loads(text)
91
129
  ok = spec in data
92
- changed = ok and data.get(spec) != version
93
130
  if ok:
131
+ current = str(data.get(spec))
132
+ changed = current != version
94
133
  data[spec] = version
95
134
  new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
135
+ else:
136
+ changed = False
96
137
  elif kind == "package-lock":
97
138
  data = json.loads(text)
98
139
  ok = "version" in data
99
- changed = ok and data.get("version") != version
100
140
  if ok:
141
+ current = str(data.get("version"))
142
+ changed = current != version
101
143
  data["version"] = version
102
144
  if "packages" in data and "" in data["packages"]:
103
145
  data["packages"][""]["version"] = version
104
146
  new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
147
+ else:
148
+ changed = False
149
+ elif kind == "openapi-spec":
150
+ data = json.loads(text)
151
+ info = data.get("info") if isinstance(data.get("info"), dict) else None
152
+ ok = info is not None and "version" in info
153
+ if ok and info is not None:
154
+ current = str(info["version"])
155
+ changed = current != version
156
+ info["version"] = version
157
+ new = _canonical_openapi_json(data)
158
+ else:
159
+ changed = False
160
+ elif kind == "openapi-envelope":
161
+ data = json.loads(text)
162
+ spec_obj = data.get("spec") if isinstance(data.get("spec"), dict) else None
163
+ info = spec_obj.get("info") if isinstance(spec_obj, dict) and isinstance(spec_obj.get("info"), dict) else None
164
+ ok = "spec_version" in data and info is not None and "version" in info
165
+ if ok and info is not None:
166
+ sv = str(data["spec_version"])
167
+ iv = str(info["version"])
168
+ current = sv if sv != version else iv
169
+ changed = sv != version or iv != version
170
+ data["spec_version"] = version
171
+ info["version"] = version
172
+ new = _canonical_openapi_json(data)
173
+ else:
174
+ changed = False
175
+ elif kind == "openapi-fragment":
176
+ data = json.loads(text)
177
+ ok = "spec_version" in data
178
+ if ok:
179
+ current = str(data["spec_version"])
180
+ changed = current != version
181
+ data["spec_version"] = version
182
+ new = _canonical_openapi_json(data)
183
+ else:
184
+ changed = False
185
+ elif kind == "openapi-index":
186
+ data = json.loads(text)
187
+ ok = "spec_version" in data
188
+ if ok:
189
+ current = str(data["spec_version"])
190
+ data["spec_version"] = version
191
+ if not check:
192
+ env_meta = data.get("envelope")
193
+ if isinstance(env_meta, dict) and "file" in env_meta:
194
+ env_path = path.parent / str(env_meta["file"])
195
+ if env_path.is_file():
196
+ env_meta["sha256"] = hashlib.sha256(env_path.read_bytes()).hexdigest()
197
+ for entry in data.get("fragments", []):
198
+ if isinstance(entry, dict) and "file" in entry:
199
+ frag_path = path.parent / str(entry["file"])
200
+ if frag_path.is_file():
201
+ entry["sha256"] = hashlib.sha256(frag_path.read_bytes()).hexdigest()
202
+ new = _canonical_openapi_json(data)
203
+ changed = current != version or new != text
204
+ else:
205
+ changed = False
105
206
  else: # pragma: no cover
106
207
  raise AssertionError(kind)
107
208
  if not ok:
@@ -109,7 +210,6 @@ def bump(version: str, *, check: bool = False) -> int:
109
210
  failures += 1
110
211
  continue
111
212
  if check:
112
- current = re.search(spec, text, flags=re.MULTILINE).group(2) if kind == "regex" else json.loads(text)["version"]
113
213
  status = "ok" if current == version else f"MISMATCH ({current})"
114
214
  if current != version:
115
215
  failures += 1
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Sharp Weight";
9
+ const releaseTheme = "Deep Vector";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12