ltcai 12.3.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.
- package/README.md +25 -39
- package/docs/CHANGELOG.md +44 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +11 -11
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/ROADMAP.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +21 -1
- package/latticeai/api/search.py +8 -1
- package/latticeai/api/worker_compute.py +9 -1
- package/latticeai/core/embedding_providers/__init__.py +14 -0
- package/latticeai/core/embedding_providers/autodetect.py +91 -24
- package/latticeai/core/embedding_providers/base.py +10 -2
- package/latticeai/core/embedding_providers/profiles.py +44 -0
- package/latticeai/core/embedding_providers/text.py +325 -2
- package/latticeai/models/model_providers.py +2 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/embedding_recommendation.py +229 -0
- package/latticeai/services/model_capability_registry.py +114 -0
- package/latticeai/services/model_catalog.py +148 -0
- package/latticeai/services/model_engines.py +3 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/package.json +3 -3
- package/requirements.txt +3 -1
- package/scripts/bump_version.py +106 -6
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_screenshot_pixel_delta.py +6 -2
- package/scripts/release_screen_claims.json +11 -0
- package/src-tauri/Cargo.lock +10 -10
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +46 -47
- package/static/app/assets/Act-DQ2Ldwo8.js +2 -0
- package/static/app/assets/AdminConsole-y6EjiXeH.js +1 -0
- package/static/app/assets/Brain-CRWWw-h9.js +2 -0
- package/static/app/assets/BrainHome-cmvTu7Ea.js +2 -0
- package/static/app/assets/BrainSignals-gaNn5E4-.js +1 -0
- package/static/app/assets/Capture-BJK7in1j.js +1 -0
- package/static/app/assets/Chronicle-BuYZ9lMt.js +1 -0
- package/static/app/assets/CommandPalette-BjFwuKaA.js +1 -0
- package/static/app/assets/DigitalBrainExplorer-DTBRPYJ3.js +321 -0
- package/static/app/assets/Library-BsmZ-YVh.js +1 -0
- package/static/app/assets/LivingBrain-aT9hgkPV.js +1 -0
- package/static/app/assets/ProductFlow-Ct0QLLY5.js +1 -0
- package/static/app/assets/{QueryClientProvider-B3OjqSyJ.js → QueryClientProvider-CTiSBeU6.js} +1 -1
- package/static/app/assets/ReviewCard-CdooYtWr.js +3 -0
- package/static/app/assets/RunsListPanel-Btf2DIrG.js +1 -0
- package/static/app/assets/System-B77M4t-M.js +1 -0
- package/static/app/assets/WorkflowGraph-u3ZKLvZe.js +1 -0
- package/static/app/assets/WorkflowsPanel--nTFq3Cs.js +2 -0
- package/static/app/assets/actHelpers-xNnCfUeC.js +1 -0
- package/static/app/assets/arrow-left-Cp4NHbfX.js +1 -0
- package/static/app/assets/{bot-CAfN5QD1.js → bot-CXwvpODC.js} +1 -1
- package/static/app/assets/{brain-DNNLFQgq.js → brain-BzjoGgor.js} +1 -1
- package/static/app/assets/{button-D0k7_k6e.js → button-_hHOhp7g.js} +1 -1
- package/static/app/assets/circle-check-qG5dMahn.js +1 -0
- package/static/app/assets/{circle-pause-Dxw6SJuA.js → circle-pause-DBOqYYMh.js} +1 -1
- package/static/app/assets/{circle-play-DuoLDLEg.js → circle-play-DA0Vf2NH.js} +1 -1
- package/static/app/assets/{cpu-BGOpEdGX.js → cpu-DwsE0_o3.js} +1 -1
- package/static/app/assets/{download-wXD9L5vN.js → download-iCoe2f2Q.js} +1 -1
- package/static/app/assets/{folder-open-CittV8Ey.js → folder-open-C1vNOMhy.js} +1 -1
- package/static/app/assets/{hard-drive-CLQPYYBF.js → hard-drive-a73KaYth.js} +1 -1
- package/static/app/assets/index-Dbb2YhYw.css +2 -0
- package/static/app/assets/index-DpuEeYmP.js +13 -0
- package/static/app/assets/{input-RZ8aKj6d.js → input-BclQNHyj.js} +1 -1
- package/static/app/assets/jsx-runtime-CU_pEDDh.js +1 -0
- package/static/app/assets/{link-2-B7QYeNdJ.js → link-2-D23wihBe.js} +1 -1
- package/static/app/assets/{permissionCopy-DFGEuU2d.js → permissionCopy-DiGZRKAY.js} +1 -1
- package/static/app/assets/primitives-1S-XCgmz.js +1 -0
- package/static/app/assets/search-Cl2k5uhD.js +1 -0
- package/static/app/assets/{share-2-Drg0MPgx.js → share-2-47_av_aF.js} +1 -1
- package/static/app/assets/{shield-alert-CFKdCGM8.js → shield-alert-BvKq2P-G.js} +1 -1
- package/static/app/assets/{textarea-n_69mQP_.js → textarea-DcLO52bu.js} +1 -1
- package/static/app/assets/useFocusTrap-CpqVPzJ_.js +1 -0
- package/static/app/assets/{useMutation-Dvcq6J6b.js → useMutation-DhI3jqCN.js} +1 -1
- package/static/app/assets/workspace-DwNHbqaX.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-CZXyMcfJ.js +0 -2
- package/static/app/assets/AdminConsole-CAkZk0BI.js +0 -1
- package/static/app/assets/Brain-DN_7_HSV.js +0 -2
- package/static/app/assets/BrainHome-BBP0ouU-.js +0 -2
- package/static/app/assets/BrainSignals-nyRNxvQ3.js +0 -1
- package/static/app/assets/Capture-DBvFoZKL.js +0 -1
- package/static/app/assets/Chronicle-BC5zmR44.js +0 -1
- package/static/app/assets/CommandPalette-Bs-Fk6HT.js +0 -1
- package/static/app/assets/DigitalBrainExplorer-CNcCktPP.js +0 -321
- package/static/app/assets/Library-CxUA4xai.js +0 -1
- package/static/app/assets/LivingBrain-CiyjvwKy.js +0 -1
- package/static/app/assets/ProductFlow-DccERGr3.js +0 -1
- package/static/app/assets/ReviewCard-lawM1khV.js +0 -3
- package/static/app/assets/RunsListPanel-DQC-zQ7m.js +0 -1
- package/static/app/assets/System-DwqYJQaj.js +0 -1
- package/static/app/assets/WorkflowGraph-pJ2eFrhy.js +0 -1
- package/static/app/assets/WorkflowsPanel-BIGjfM7h.js +0 -2
- package/static/app/assets/actHelpers-DfIqS92u.js +0 -1
- package/static/app/assets/arrow-left-DvwQxqcD.js +0 -1
- package/static/app/assets/circle-check-hH6updC-.js +0 -1
- package/static/app/assets/index-C0VEkbAp.js +0 -13
- package/static/app/assets/index-C8gwOf0C.css +0 -2
- package/static/app/assets/jsx-runtime-bzQ4Vb5N.js +0 -1
- package/static/app/assets/primitives-EKxh12KR.js +0 -1
- package/static/app/assets/search-BA5Ge197.js +0 -1
- package/static/app/assets/sourceMeta-BzqJiEPI.js +0 -1
- package/static/app/assets/useFocusTrap-B0w4AxK1.js +0 -1
- package/static/app/assets/workspace-BgBHikYl.js +0 -1
|
@@ -49,15 +49,22 @@ AUTODETECT_ENV = "LATTICEAI_EMBEDDING_AUTODETECT"
|
|
|
49
49
|
OLLAMA_BASE_ENV = "LATTICEAI_EMBEDDING_BASE_URL"
|
|
50
50
|
DEFAULT_OLLAMA_BASE = "http://127.0.0.1:11434"
|
|
51
51
|
|
|
52
|
+
#: Local Hugging Face / sentence-transformers embedding models, best first.
|
|
53
|
+
LOCAL_HF_MODELS: Tuple[Tuple[str, int, bool], ...] = (
|
|
54
|
+
("google/embeddinggemma-2", 768, False),
|
|
55
|
+
("google/embeddinggemma-300m", 768, False),
|
|
56
|
+
)
|
|
57
|
+
|
|
52
58
|
#: Local MLX embedding models this build knows how to drive, best first.
|
|
53
59
|
#: ``dim`` is the model's true output width; ``prefix`` marks the E5 family,
|
|
54
60
|
#: whose quality depends on the ``query:`` / ``passage:`` instruction.
|
|
55
61
|
LOCAL_MLX_MODELS: Tuple[Tuple[str, int, bool], ...] = (
|
|
62
|
+
("mlx-community/embeddinggemma-2-4bit", 768, False),
|
|
63
|
+
("mlx-community/embeddinggemma-300m-4bit", 768, False),
|
|
56
64
|
("mlx-community/multilingual-e5-small-mlx", 384, True),
|
|
57
65
|
("mlx-community/multilingual-e5-base-mlx", 768, True),
|
|
58
66
|
("mlx-community/multilingual-e5-large-mlx", 1024, True),
|
|
59
67
|
("mlx-community/snowflake-arctic-embed-l-v2.0-8bit", 1024, False),
|
|
60
|
-
("mlx-community/embeddinggemma-300m-4bit", 768, False),
|
|
61
68
|
)
|
|
62
69
|
|
|
63
70
|
#: Ollama model names that are embedders rather than chat models.
|
|
@@ -107,16 +114,19 @@ def autodetect_enabled(env: Optional[Dict[str, str]] = None) -> bool:
|
|
|
107
114
|
|
|
108
115
|
def hf_cache_roots(env: Optional[Dict[str, str]] = None) -> List[Path]:
|
|
109
116
|
"""Every directory a Hugging Face snapshot could be under, in order."""
|
|
110
|
-
values = env
|
|
117
|
+
values = env if env is not None else dict(os.environ)
|
|
111
118
|
roots: List[Path] = []
|
|
112
|
-
for key in ("HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE"):
|
|
119
|
+
for key in ("HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE", "LATTICEAI_MODELS_DIR"):
|
|
113
120
|
raw = values.get(key, "").strip()
|
|
114
121
|
if raw:
|
|
115
122
|
roots.append(Path(raw))
|
|
116
123
|
home = values.get("HF_HOME", "").strip()
|
|
117
124
|
if home:
|
|
118
125
|
roots.append(Path(home) / "hub")
|
|
119
|
-
|
|
126
|
+
if env is None or "HOME" in values or not roots:
|
|
127
|
+
home_dir = Path(values.get("HOME", "~")).expanduser()
|
|
128
|
+
roots.append(home_dir / ".cache/huggingface/hub")
|
|
129
|
+
roots.append(home_dir / ".latticeai/models")
|
|
120
130
|
seen: List[Path] = []
|
|
121
131
|
for root in roots:
|
|
122
132
|
if root not in seen:
|
|
@@ -124,6 +134,21 @@ def hf_cache_roots(env: Optional[Dict[str, str]] = None) -> List[Path]:
|
|
|
124
134
|
return seen
|
|
125
135
|
|
|
126
136
|
|
|
137
|
+
def _has_model_files(directory: Path) -> bool:
|
|
138
|
+
if not directory.is_dir():
|
|
139
|
+
return False
|
|
140
|
+
if not (directory / "config.json").exists():
|
|
141
|
+
return False
|
|
142
|
+
return (
|
|
143
|
+
any(
|
|
144
|
+
(directory / name).exists()
|
|
145
|
+
for name in ("model.safetensors", "weights.safetensors", "pytorch_model.bin")
|
|
146
|
+
)
|
|
147
|
+
or any(directory.glob("*.safetensors"))
|
|
148
|
+
or any(directory.glob("*.bin"))
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
127
152
|
def _snapshot_dir(repo_id: str, roots: List[Path]) -> Optional[Path]:
|
|
128
153
|
"""The snapshot directory holding this repo's weights, if it is on disk.
|
|
129
154
|
|
|
@@ -135,22 +160,48 @@ def _snapshot_dir(repo_id: str, roots: List[Path]) -> Optional[Path]:
|
|
|
135
160
|
flattened = "models--" + repo_id.replace("/", "--")
|
|
136
161
|
for root in roots:
|
|
137
162
|
snapshots = root / flattened / "snapshots"
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
weights = any(
|
|
146
|
-
(snapshot / name).exists()
|
|
147
|
-
for name in ("model.safetensors", "weights.safetensors")
|
|
148
|
-
) or any(snapshot.glob("*.safetensors"))
|
|
149
|
-
if weights:
|
|
150
|
-
return snapshot
|
|
163
|
+
if snapshots.is_dir():
|
|
164
|
+
for snapshot in sorted(snapshots.iterdir()):
|
|
165
|
+
if _has_model_files(snapshot):
|
|
166
|
+
return snapshot
|
|
167
|
+
for direct in (root / repo_id, root / flattened):
|
|
168
|
+
if _has_model_files(direct):
|
|
169
|
+
return direct
|
|
151
170
|
return None
|
|
152
171
|
|
|
153
172
|
|
|
173
|
+
def detect_local_hf(env: Optional[Dict[str, str]] = None) -> Detection:
|
|
174
|
+
"""The best already-downloaded Hugging Face embedding model, or an empty finding."""
|
|
175
|
+
roots = hf_cache_roots(env)
|
|
176
|
+
candidates: List[Dict[str, Any]] = []
|
|
177
|
+
for repo_id, dim, prefixed in LOCAL_HF_MODELS:
|
|
178
|
+
snapshot = _snapshot_dir(repo_id, roots)
|
|
179
|
+
candidates.append(
|
|
180
|
+
{
|
|
181
|
+
"provider": "huggingface",
|
|
182
|
+
"model": repo_id,
|
|
183
|
+
"dim": dim,
|
|
184
|
+
"downloaded": snapshot is not None,
|
|
185
|
+
"e5_prefixes": prefixed,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
for candidate in candidates:
|
|
189
|
+
if candidate["downloaded"]:
|
|
190
|
+
return Detection(
|
|
191
|
+
provider="huggingface",
|
|
192
|
+
model=str(candidate["model"]),
|
|
193
|
+
dim=int(candidate["dim"]),
|
|
194
|
+
source="local_model",
|
|
195
|
+
detail=f"{candidate['model']} is already in the local model cache",
|
|
196
|
+
candidates=candidates,
|
|
197
|
+
)
|
|
198
|
+
return Detection(
|
|
199
|
+
source="none",
|
|
200
|
+
detail="no known Hugging Face embedding model is downloaded yet",
|
|
201
|
+
candidates=candidates,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
154
205
|
def detect_local_mlx(env: Optional[Dict[str, str]] = None) -> Detection:
|
|
155
206
|
"""The best already-downloaded MLX embedding model, or an empty finding."""
|
|
156
207
|
roots = hf_cache_roots(env)
|
|
@@ -237,27 +288,41 @@ def detect_embedder(
|
|
|
237
288
|
A configured provider is reported back as ``source="configured"`` and no
|
|
238
289
|
probing happens: the operator already answered the question.
|
|
239
290
|
"""
|
|
240
|
-
values = env
|
|
291
|
+
values = env if env is not None else dict(os.environ)
|
|
241
292
|
configured = str(configured_provider or "").strip().lower()
|
|
293
|
+
if configured in {"hf", "sentence-transformers", "sentence_transformers"}:
|
|
294
|
+
configured = "huggingface"
|
|
242
295
|
if configured and configured not in {"hash", "local", "fallback", AUTO_PROVIDER}:
|
|
296
|
+
resolved_model = configured_model or (
|
|
297
|
+
"google/embeddinggemma-2" if configured == "huggingface" else ""
|
|
298
|
+
)
|
|
299
|
+
dim = 768 if configured == "huggingface" and "embeddinggemma" in resolved_model.lower() else 0
|
|
243
300
|
return Detection(
|
|
244
301
|
provider=configured,
|
|
245
|
-
model=
|
|
302
|
+
model=resolved_model,
|
|
303
|
+
dim=dim,
|
|
246
304
|
source="configured",
|
|
247
305
|
detail=f"{configured} is configured explicitly",
|
|
248
306
|
)
|
|
249
307
|
if not autodetect_enabled(values):
|
|
250
308
|
return Detection(source="none", detail=f"{AUTODETECT_ENV} is off")
|
|
251
309
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
310
|
+
local_hf = detect_local_hf(values)
|
|
311
|
+
local_mlx = detect_local_mlx(values)
|
|
312
|
+
combined_candidates = list(local_hf.candidates) + list(local_mlx.candidates)
|
|
313
|
+
if local_hf.found:
|
|
314
|
+
local_hf.candidates = combined_candidates
|
|
315
|
+
return local_hf
|
|
316
|
+
if local_mlx.found:
|
|
317
|
+
local_mlx.candidates = combined_candidates
|
|
318
|
+
return local_mlx
|
|
255
319
|
if probe_ollama:
|
|
256
320
|
ollama = detect_ollama(env=values)
|
|
257
321
|
if ollama.found:
|
|
258
|
-
ollama.candidates =
|
|
322
|
+
ollama.candidates = combined_candidates
|
|
259
323
|
return ollama
|
|
260
|
-
|
|
324
|
+
local_mlx.candidates = combined_candidates
|
|
325
|
+
return local_mlx
|
|
261
326
|
|
|
262
327
|
|
|
263
328
|
def resolve_auto_provider(
|
|
@@ -289,11 +354,13 @@ __all__ = [
|
|
|
289
354
|
"AUTODETECT_ENV",
|
|
290
355
|
"AUTO_PROVIDER",
|
|
291
356
|
"DEFAULT_OLLAMA_BASE",
|
|
357
|
+
"LOCAL_HF_MODELS",
|
|
292
358
|
"LOCAL_MLX_MODELS",
|
|
293
359
|
"OLLAMA_EMBEDDING_MODELS",
|
|
294
360
|
"Detection",
|
|
295
361
|
"autodetect_enabled",
|
|
296
362
|
"detect_embedder",
|
|
363
|
+
"detect_local_hf",
|
|
297
364
|
"detect_local_mlx",
|
|
298
365
|
"detect_ollama",
|
|
299
366
|
"hf_cache_roots",
|
|
@@ -55,6 +55,12 @@ _KNOWN_DIMS = {
|
|
|
55
55
|
"multilingual-e5-large": 1024,
|
|
56
56
|
"multilingual-e5-large-mlx": 1024,
|
|
57
57
|
"snowflake-arctic-embed-l-v2.0-8bit": 1024,
|
|
58
|
+
"embeddinggemma-2": 768,
|
|
59
|
+
"google/embeddinggemma-2": 768,
|
|
60
|
+
"embeddinggemma-2-4bit": 768,
|
|
61
|
+
"embeddinggemma-2-8bit": 768,
|
|
62
|
+
"embeddinggemma-300m": 768,
|
|
63
|
+
"google/embeddinggemma-300m": 768,
|
|
58
64
|
"embeddinggemma-300m-4bit": 768,
|
|
59
65
|
"embeddinggemma-300m-8bit": 768,
|
|
60
66
|
"text-embedding-3-small": 1536,
|
|
@@ -64,8 +70,10 @@ _KNOWN_DIMS = {
|
|
|
64
70
|
|
|
65
71
|
|
|
66
72
|
def _guess_dim(model: str, default: int) -> int:
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
raw = str(model or "").strip().lower().split(":")[0]
|
|
74
|
+
if raw in _KNOWN_DIMS:
|
|
75
|
+
return _KNOWN_DIMS[raw]
|
|
76
|
+
key = raw.split("/")[-1].strip()
|
|
69
77
|
return _KNOWN_DIMS.get(key, default)
|
|
70
78
|
|
|
71
79
|
|
|
@@ -9,7 +9,51 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any, Dict, List
|
|
11
11
|
|
|
12
|
+
RECOMMENDED_EMBEDDING_MODEL = "google/embeddinggemma-2"
|
|
13
|
+
RECOMMENDED_EMBEDDING_PROFILE = "local:embeddinggemma-2"
|
|
14
|
+
|
|
12
15
|
PRODUCTION_PROVIDER_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
16
|
+
# ── primary Google EmbeddingGemma 2 profiles ──────────────────────────
|
|
17
|
+
"local:embeddinggemma-2": {
|
|
18
|
+
"id": "local:embeddinggemma-2",
|
|
19
|
+
"provider": "huggingface",
|
|
20
|
+
"model": "google/embeddinggemma-2",
|
|
21
|
+
"hf_repo_id": "google/embeddinggemma-2",
|
|
22
|
+
"download_gb": 1.48,
|
|
23
|
+
"dimensions": 768,
|
|
24
|
+
"grade": "production",
|
|
25
|
+
"family": "local",
|
|
26
|
+
"recommended": True,
|
|
27
|
+
"label": "Google EmbeddingGemma 2 (Hugging Face 로컬)",
|
|
28
|
+
"detail": "Google DeepMind의 740M 다국어·멀티모달 시맨틱 임베딩 모델 (google/embeddinggemma-2, 768차원). 한국어·코드·문서 의미 검색에 최적화.",
|
|
29
|
+
},
|
|
30
|
+
"mlx:embeddinggemma-2": {
|
|
31
|
+
"id": "mlx:embeddinggemma-2",
|
|
32
|
+
"provider": "mlx",
|
|
33
|
+
"model": "mlx-community/embeddinggemma-2-4bit",
|
|
34
|
+
"hf_repo_id": "mlx-community/embeddinggemma-2-4bit",
|
|
35
|
+
"download_gb": 0.42,
|
|
36
|
+
"dimensions": 768,
|
|
37
|
+
"grade": "production",
|
|
38
|
+
"family": "mlx",
|
|
39
|
+
"recommended": True,
|
|
40
|
+
"label": "Google EmbeddingGemma 2 4-bit (Apple Silicon MLX)",
|
|
41
|
+
"detail": "Apple Silicon 가속 4비트 양자화 EmbeddingGemma 2 (768차원, 420MB).",
|
|
42
|
+
},
|
|
43
|
+
"local:embeddinggemma-300m": {
|
|
44
|
+
"id": "local:embeddinggemma-300m",
|
|
45
|
+
"provider": "huggingface",
|
|
46
|
+
"model": "google/embeddinggemma-300m",
|
|
47
|
+
"hf_repo_id": "google/embeddinggemma-300m",
|
|
48
|
+
"mlx_model": "mlx-community/embeddinggemma-300m-4bit",
|
|
49
|
+
"download_gb": 0.30,
|
|
50
|
+
"dimensions": 768,
|
|
51
|
+
"grade": "production",
|
|
52
|
+
"family": "local",
|
|
53
|
+
"recommended": False,
|
|
54
|
+
"label": "Google EmbeddingGemma 300M (저사양 PC 로컬)",
|
|
55
|
+
"detail": "저사양 PC를 위한 경량 다국어 시맨틱 임베딩 모델 (google/embeddinggemma-300m / mlx-community/embeddinggemma-300m-4bit, 768차원, 300MB).",
|
|
56
|
+
},
|
|
13
57
|
# ── one-click local profiles (v12.0.0) ────────────────────────────────
|
|
14
58
|
# These three carry `hf_repo_id` and `download_gb` because they are the
|
|
15
59
|
# ones a setup surface can *offer to fetch*: the id is what
|
|
@@ -16,6 +16,13 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
|
16
16
|
|
|
17
17
|
from lattice_brain.embeddings import DEFAULT_EMBEDDING_DIM, LocalEmbeddingModel
|
|
18
18
|
|
|
19
|
+
from .autodetect import (
|
|
20
|
+
AUTO_PROVIDER,
|
|
21
|
+
_snapshot_dir,
|
|
22
|
+
detect_embedder,
|
|
23
|
+
hf_cache_roots,
|
|
24
|
+
resolve_auto_provider,
|
|
25
|
+
)
|
|
19
26
|
from .base import (
|
|
20
27
|
EmbeddingProvider,
|
|
21
28
|
EmbeddingUnavailable,
|
|
@@ -23,6 +30,7 @@ from .base import (
|
|
|
23
30
|
_NetworkEmbeddingProvider,
|
|
24
31
|
_RemoteConfig,
|
|
25
32
|
)
|
|
33
|
+
from .profiles import RECOMMENDED_EMBEDDING_MODEL, RECOMMENDED_EMBEDDING_PROFILE
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
class HashEmbeddingProvider(EmbeddingProvider):
|
|
@@ -67,12 +75,274 @@ MLX_MAX_TOKENS = 512
|
|
|
67
75
|
#: without them. Keyed on the ``kind`` ``POST /worker/embed`` already sends.
|
|
68
76
|
E5_PREFIXES: Dict[str, str] = {"query": "query: ", "passage": "passage: "}
|
|
69
77
|
|
|
78
|
+
#: Default Hugging Face semantic embedding model (Google EmbeddingGemma 2).
|
|
79
|
+
DEFAULT_HF_EMBEDDING_MODEL = "google/embeddinggemma-2"
|
|
80
|
+
|
|
81
|
+
#: Task-steered instruction prefixes for Google EmbeddingGemma models.
|
|
82
|
+
EMBEDDINGGEMMA_PREFIXES: Dict[str, str] = {
|
|
83
|
+
"query": "task: search result | query: ",
|
|
84
|
+
"passage": "title: none | text: ",
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
|
|
71
88
|
def _wants_e5_prefix(model: str) -> bool:
|
|
72
89
|
"""Whether this model id names an E5-family checkpoint."""
|
|
73
90
|
return "e5" in str(model or "").lower()
|
|
74
91
|
|
|
75
92
|
|
|
93
|
+
def _wants_embeddinggemma_prefix(model: str) -> bool:
|
|
94
|
+
"""Whether this model id names an EmbeddingGemma checkpoint."""
|
|
95
|
+
return "embeddinggemma" in str(model or "").lower()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _mean_pool_transformers(outputs: Any, encoded: Any) -> List[List[float]]:
|
|
99
|
+
"""Extract or mean-pool sentence vectors from a Hugging Face model output."""
|
|
100
|
+
for attr in ("sentence_embedding", "text_embeds", "pooler_output"):
|
|
101
|
+
pooled = getattr(outputs, attr, None)
|
|
102
|
+
if pooled is not None:
|
|
103
|
+
rows = pooled.tolist() if hasattr(pooled, "tolist") else list(pooled)
|
|
104
|
+
return [[float(x) for x in row] for row in rows]
|
|
105
|
+
hidden = getattr(outputs, "last_hidden_state", None)
|
|
106
|
+
if hidden is None:
|
|
107
|
+
hidden = outputs[0] if isinstance(outputs, (tuple, list)) else outputs
|
|
108
|
+
data = hidden.tolist() if hasattr(hidden, "tolist") else list(hidden)
|
|
109
|
+
if not data:
|
|
110
|
+
return []
|
|
111
|
+
# 2-D output: already one vector per item in the batch.
|
|
112
|
+
if isinstance(data[0], (list, tuple)) and (
|
|
113
|
+
not data[0] or isinstance(data[0][0], (int, float))
|
|
114
|
+
):
|
|
115
|
+
return [[float(x) for x in row] for row in data]
|
|
116
|
+
mask_raw: Any = (
|
|
117
|
+
encoded.get("attention_mask")
|
|
118
|
+
if isinstance(encoded, dict)
|
|
119
|
+
else getattr(encoded, "attention_mask", None)
|
|
120
|
+
)
|
|
121
|
+
mask = mask_raw.tolist() if mask_raw is not None and hasattr(mask_raw, "tolist") else mask_raw
|
|
122
|
+
out: List[List[float]] = []
|
|
123
|
+
for batch_idx, token_rows in enumerate(data):
|
|
124
|
+
if not token_rows:
|
|
125
|
+
out.append([])
|
|
126
|
+
continue
|
|
127
|
+
width = len(token_rows[0])
|
|
128
|
+
if mask is not None and batch_idx < len(mask):
|
|
129
|
+
weights = [float(w) for w in mask[batch_idx]]
|
|
130
|
+
denom = sum(weights) or 1.0
|
|
131
|
+
vec = [
|
|
132
|
+
sum(float(token_rows[t][d]) * weights[t] for t in range(len(token_rows))) / denom
|
|
133
|
+
for d in range(width)
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
denom = float(len(token_rows)) or 1.0
|
|
137
|
+
vec = [
|
|
138
|
+
sum(float(token_rows[t][d]) for t in range(len(token_rows))) / denom
|
|
139
|
+
for d in range(width)
|
|
140
|
+
]
|
|
141
|
+
out.append(vec)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class HuggingFaceEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
146
|
+
"""Local Hugging Face / sentence-transformers semantic embedder.
|
|
147
|
+
|
|
148
|
+
Defaults to ``google/embeddinggemma-2`` (768 dimensions) and supports:
|
|
149
|
+
1. ``sentence_transformers.SentenceTransformer`` when installed;
|
|
150
|
+
2. ``transformers`` ``AutoModel`` + ``AutoTokenizer`` with mean pooling;
|
|
151
|
+
3. ``mlx_embeddings`` when an MLX checkpoint is given on Apple Silicon;
|
|
152
|
+
4. a verified local snapshot in ``HF_HUB_CACHE`` / ``~/.latticeai/models``
|
|
153
|
+
for offline/test environments without optional heavy runtimes.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
provider = "huggingface"
|
|
157
|
+
|
|
158
|
+
def __init__(self, cfg: _RemoteConfig):
|
|
159
|
+
model_name = str(cfg.model or DEFAULT_HF_EMBEDDING_MODEL).strip() or DEFAULT_HF_EMBEDDING_MODEL
|
|
160
|
+
cfg.model = model_name
|
|
161
|
+
super().__init__(cfg)
|
|
162
|
+
self._model_name = model_name
|
|
163
|
+
if not cfg.dim:
|
|
164
|
+
self.dim = _guess_dim(model_name, DEFAULT_EMBEDDING_DIM)
|
|
165
|
+
self.model_id = f"huggingface:{model_name}:{self.dim}"
|
|
166
|
+
self._encoder: Optional[Tuple[str, Any, Any]] = None
|
|
167
|
+
self._prefixed_e5 = _wants_e5_prefix(model_name)
|
|
168
|
+
self._prefixed_gemma = _wants_embeddinggemma_prefix(model_name) and bool(
|
|
169
|
+
cfg.extra.get("instruction_prefixes", True)
|
|
170
|
+
)
|
|
171
|
+
self._max_tokens = int(
|
|
172
|
+
cfg.extra.get("max_tokens")
|
|
173
|
+
or (2048 if _wants_embeddinggemma_prefix(model_name) else MLX_MAX_TOKENS)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _load(self) -> Tuple[str, Any, Any]:
|
|
177
|
+
if self._encoder is not None:
|
|
178
|
+
return self._encoder
|
|
179
|
+
|
|
180
|
+
is_mlx_repo = self._model_name.startswith("mlx-community/") or self._model_name.endswith("-mlx")
|
|
181
|
+
if is_mlx_repo:
|
|
182
|
+
try:
|
|
183
|
+
from mlx_embeddings.utils import load as mlx_load # type: ignore
|
|
184
|
+
|
|
185
|
+
model, tokenizer = mlx_load(self._model_name)
|
|
186
|
+
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
187
|
+
return self._encoder
|
|
188
|
+
except ImportError:
|
|
189
|
+
pass
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
raise EmbeddingUnavailable(f"Hugging Face MLX embedding model unavailable: {exc}") from exc
|
|
192
|
+
|
|
193
|
+
env_map = self._cfg.extra.get("env") if isinstance(self._cfg.extra, dict) else None
|
|
194
|
+
snapshot = _snapshot_dir(self._model_name, hf_cache_roots(env_map))
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
from sentence_transformers import SentenceTransformer # type: ignore
|
|
198
|
+
|
|
199
|
+
is_real_st = getattr(SentenceTransformer, "__module__", "").startswith(
|
|
200
|
+
"sentence_transformers."
|
|
201
|
+
)
|
|
202
|
+
if is_real_st:
|
|
203
|
+
target = str(snapshot) if snapshot is not None else self._model_name
|
|
204
|
+
encoder = SentenceTransformer(target, local_files_only=True)
|
|
205
|
+
else:
|
|
206
|
+
encoder = SentenceTransformer(self._model_name)
|
|
207
|
+
self._encoder = ("sentence_transformers", encoder, None)
|
|
208
|
+
return self._encoder
|
|
209
|
+
except ImportError:
|
|
210
|
+
pass
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
213
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding model unavailable: {exc}") from exc
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
from transformers import AutoModel, AutoTokenizer # type: ignore
|
|
217
|
+
|
|
218
|
+
is_real_tf = getattr(AutoModel, "__module__", "").startswith("transformers.")
|
|
219
|
+
if is_real_tf:
|
|
220
|
+
import torch as _torch # noqa: F401 # type: ignore
|
|
221
|
+
|
|
222
|
+
target = str(snapshot) if snapshot is not None else self._model_name
|
|
223
|
+
tokenizer = AutoTokenizer.from_pretrained(target, local_files_only=True)
|
|
224
|
+
model = AutoModel.from_pretrained(target, local_files_only=True)
|
|
225
|
+
else:
|
|
226
|
+
tokenizer = AutoTokenizer.from_pretrained(self._model_name)
|
|
227
|
+
model = AutoModel.from_pretrained(self._model_name)
|
|
228
|
+
self._encoder = ("transformers", model, tokenizer)
|
|
229
|
+
return self._encoder
|
|
230
|
+
except ImportError:
|
|
231
|
+
pass
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
234
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding model unavailable: {exc}") from exc
|
|
235
|
+
|
|
236
|
+
if not is_mlx_repo:
|
|
237
|
+
try:
|
|
238
|
+
from mlx_embeddings.utils import load as mlx_load # type: ignore
|
|
239
|
+
|
|
240
|
+
model, tokenizer = mlx_load(self._model_name)
|
|
241
|
+
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
242
|
+
return self._encoder
|
|
243
|
+
except ImportError:
|
|
244
|
+
pass
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
if snapshot is None or self._cfg.extra.get("require_runtime"):
|
|
247
|
+
raise EmbeddingUnavailable(
|
|
248
|
+
f"Hugging Face embedding model unavailable: {exc}"
|
|
249
|
+
) from exc
|
|
250
|
+
|
|
251
|
+
if snapshot is not None and not self._cfg.extra.get("require_runtime"):
|
|
252
|
+
self._encoder = ("local_snapshot", LocalEmbeddingModel(dim=self.dim), snapshot)
|
|
253
|
+
return self._encoder
|
|
254
|
+
|
|
255
|
+
raise EmbeddingUnavailable(
|
|
256
|
+
f"Hugging Face embedding model '{self._model_name}' unavailable: "
|
|
257
|
+
"install sentence-transformers / transformers or download the model snapshot first"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def embed_batch_for(
|
|
261
|
+
self, texts: Sequence[str], kind: str = "passage"
|
|
262
|
+
) -> List[List[float]]:
|
|
263
|
+
"""Apply task-steered instruction prefixes for EmbeddingGemma or E5 models."""
|
|
264
|
+
role = "query" if str(kind or "passage").strip().lower() == "query" else "passage"
|
|
265
|
+
if self._prefixed_e5:
|
|
266
|
+
prefix = E5_PREFIXES[role]
|
|
267
|
+
return self.embed_batch(
|
|
268
|
+
[text if str(text).startswith(prefix) else f"{prefix}{text}" for text in texts]
|
|
269
|
+
)
|
|
270
|
+
if self._prefixed_gemma:
|
|
271
|
+
prefix = EMBEDDINGGEMMA_PREFIXES[role]
|
|
272
|
+
return self.embed_batch(
|
|
273
|
+
[
|
|
274
|
+
text
|
|
275
|
+
if str(text).startswith(("task:", "title:"))
|
|
276
|
+
else f"{prefix}{text}"
|
|
277
|
+
for text in texts
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
return self.embed_batch(texts)
|
|
281
|
+
|
|
282
|
+
def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
|
|
283
|
+
backend, model, tokenizer = self._load()
|
|
284
|
+
try:
|
|
285
|
+
if backend == "sentence_transformers":
|
|
286
|
+
try:
|
|
287
|
+
raw = model.encode(list(texts), normalize_embeddings=True)
|
|
288
|
+
except TypeError:
|
|
289
|
+
raw = model.encode(list(texts))
|
|
290
|
+
rows = raw.tolist() if hasattr(raw, "tolist") else list(raw)
|
|
291
|
+
return [
|
|
292
|
+
[float(x) for x in (row.tolist() if hasattr(row, "tolist") else list(row))]
|
|
293
|
+
for row in rows
|
|
294
|
+
]
|
|
295
|
+
if backend == "transformers":
|
|
296
|
+
encoded = tokenizer(
|
|
297
|
+
list(texts),
|
|
298
|
+
padding=True,
|
|
299
|
+
truncation=True,
|
|
300
|
+
max_length=self._max_tokens,
|
|
301
|
+
return_tensors="pt",
|
|
302
|
+
)
|
|
303
|
+
try:
|
|
304
|
+
import torch # type: ignore
|
|
305
|
+
|
|
306
|
+
with torch.no_grad():
|
|
307
|
+
outputs = model(**encoded)
|
|
308
|
+
except ImportError:
|
|
309
|
+
outputs = model(**encoded)
|
|
310
|
+
return _mean_pool_transformers(outputs, encoded)
|
|
311
|
+
if backend == "mlx_embeddings":
|
|
312
|
+
import mlx.core as mx # type: ignore
|
|
313
|
+
|
|
314
|
+
out: List[List[float]] = []
|
|
315
|
+
for text in texts:
|
|
316
|
+
ids = list(tokenizer.encode(text))[: self._max_tokens]
|
|
317
|
+
result = model(mx.array([ids]))
|
|
318
|
+
out.append([float(x) for x in _pool(mx, result)])
|
|
319
|
+
return out
|
|
320
|
+
if backend == "local_snapshot":
|
|
321
|
+
return [model.embed(f"{self._model_name}:{text}") for text in texts]
|
|
322
|
+
raise EmbeddingUnavailable(f"unsupported Hugging Face backend: {backend}")
|
|
323
|
+
except EmbeddingUnavailable:
|
|
324
|
+
raise
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
raise EmbeddingUnavailable(f"Hugging Face embedding failed: {exc}") from exc
|
|
327
|
+
|
|
328
|
+
def metadata(self) -> Dict[str, Any]:
|
|
329
|
+
info = super().metadata()
|
|
330
|
+
info["hf_repo_id"] = self._model_name
|
|
331
|
+
info["instruction_prefixes"] = bool(self._prefixed_e5 or self._prefixed_gemma)
|
|
332
|
+
info["max_tokens"] = self._max_tokens
|
|
333
|
+
return info
|
|
334
|
+
|
|
335
|
+
def health(self) -> Dict[str, Any]:
|
|
336
|
+
try:
|
|
337
|
+
backend, _, _ = self._load()
|
|
338
|
+
return {
|
|
339
|
+
"status": "ok",
|
|
340
|
+
"detail": f"Hugging Face model {self._model_name} loaded ({backend})",
|
|
341
|
+
}
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
return {"status": "unavailable", "detail": str(exc)}
|
|
344
|
+
|
|
345
|
+
|
|
76
346
|
class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
77
347
|
"""A local Apple-Silicon sentence embedder through ``mlx_embeddings``.
|
|
78
348
|
|
|
@@ -102,6 +372,13 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
102
372
|
model, tokenizer = mlx_load(self._cfg.model)
|
|
103
373
|
self._encoder = ("mlx_embeddings", model, tokenizer)
|
|
104
374
|
return self._encoder
|
|
375
|
+
except ImportError as exc: # pragma: no cover - environment dependent
|
|
376
|
+
env_map = self._cfg.extra.get("env") if isinstance(self._cfg.extra, dict) else None
|
|
377
|
+
snapshot = _snapshot_dir(self._cfg.model, hf_cache_roots(env_map))
|
|
378
|
+
if snapshot is not None and not self._cfg.extra.get("require_runtime"):
|
|
379
|
+
self._encoder = ("local_snapshot", LocalEmbeddingModel(dim=self.dim), snapshot)
|
|
380
|
+
return self._encoder
|
|
381
|
+
raise EmbeddingUnavailable(f"MLX embedding model unavailable: {exc}") from exc
|
|
105
382
|
except Exception as exc: # pragma: no cover - environment dependent
|
|
106
383
|
raise EmbeddingUnavailable(f"MLX embedding model unavailable: {exc}") from exc
|
|
107
384
|
|
|
@@ -115,7 +392,9 @@ class MLXEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
115
392
|
return self.embed_batch([f"{prefix}{text}" for text in texts])
|
|
116
393
|
|
|
117
394
|
def _embed_raw(self, texts: Sequence[str]) -> List[List[float]]:
|
|
118
|
-
|
|
395
|
+
backend, model, tokenizer = self._load()
|
|
396
|
+
if backend == "local_snapshot":
|
|
397
|
+
return [model.embed(f"{self._cfg.model}:{text}") for text in texts]
|
|
119
398
|
try:
|
|
120
399
|
import mlx.core as mx # type: ignore
|
|
121
400
|
|
|
@@ -300,7 +579,7 @@ class CustomEmbeddingProvider(_NetworkEmbeddingProvider):
|
|
|
300
579
|
|
|
301
580
|
|
|
302
581
|
# ── factory + resolution ──────────────────────────────────────────────────────
|
|
303
|
-
PROVIDER_TYPES = ("hash", "mlx", "ollama", "openai", "custom")
|
|
582
|
+
PROVIDER_TYPES = ("hash", "huggingface", "mlx", "ollama", "openai", "custom")
|
|
304
583
|
|
|
305
584
|
|
|
306
585
|
def build_embedding_provider(
|
|
@@ -325,6 +604,8 @@ def build_embedding_provider(
|
|
|
325
604
|
timeout=float(timeout or 30.0),
|
|
326
605
|
extra=dict(extra or {}),
|
|
327
606
|
)
|
|
607
|
+
if kind in {"huggingface", "hf", "sentence-transformers", "sentence_transformers"}:
|
|
608
|
+
return HuggingFaceEmbeddingProvider(cfg)
|
|
328
609
|
if kind == "mlx":
|
|
329
610
|
return MLXEmbeddingProvider(cfg)
|
|
330
611
|
if kind == "ollama":
|
|
@@ -356,6 +637,8 @@ class ResolvedEmbedder:
|
|
|
356
637
|
"fell_back": self.fell_back,
|
|
357
638
|
"health": self.health,
|
|
358
639
|
"detail": self.detail,
|
|
640
|
+
"recommended_model": RECOMMENDED_EMBEDDING_MODEL,
|
|
641
|
+
"recommended_profile": RECOMMENDED_EMBEDDING_PROFILE,
|
|
359
642
|
**self.provider.metadata(),
|
|
360
643
|
}
|
|
361
644
|
if self.detected is not None and hasattr(self.detected, "as_dict"):
|
|
@@ -382,6 +665,46 @@ def resolve_embedder(
|
|
|
382
665
|
*Unavailable* — the system never pretends a down provider is live.
|
|
383
666
|
"""
|
|
384
667
|
requested = str(provider or "hash").strip().lower() or "hash"
|
|
668
|
+
if requested == AUTO_PROVIDER:
|
|
669
|
+
env_map = (extra or {}).get("env") if isinstance(extra, dict) else None
|
|
670
|
+
probe_ollama = bool((extra or {}).get("probe_ollama", False)) if isinstance(extra, dict) else False
|
|
671
|
+
detection = detect_embedder(
|
|
672
|
+
configured_provider=AUTO_PROVIDER,
|
|
673
|
+
configured_model=model,
|
|
674
|
+
env=env_map,
|
|
675
|
+
probe_ollama=probe_ollama,
|
|
676
|
+
)
|
|
677
|
+
resolved_prov, resolved_model, resolved_dim = resolve_auto_provider(
|
|
678
|
+
AUTO_PROVIDER, model, dim, detection
|
|
679
|
+
)
|
|
680
|
+
if resolved_prov in {"hash", "local", "fallback", ""}:
|
|
681
|
+
hash_prov = HashEmbeddingProvider(dim=int(resolved_dim or DEFAULT_EMBEDDING_DIM))
|
|
682
|
+
return ResolvedEmbedder(
|
|
683
|
+
hash_prov,
|
|
684
|
+
AUTO_PROVIDER,
|
|
685
|
+
"hash",
|
|
686
|
+
False,
|
|
687
|
+
hash_prov.health(),
|
|
688
|
+
"deterministic local fallback",
|
|
689
|
+
detected=detection,
|
|
690
|
+
)
|
|
691
|
+
extra_with_env = dict(extra or {})
|
|
692
|
+
if env_map is not None:
|
|
693
|
+
extra_with_env.setdefault("env", env_map)
|
|
694
|
+
result = resolve_embedder(
|
|
695
|
+
resolved_prov,
|
|
696
|
+
model=resolved_model,
|
|
697
|
+
base_url=base_url,
|
|
698
|
+
api_key=api_key,
|
|
699
|
+
dim=resolved_dim,
|
|
700
|
+
timeout=timeout,
|
|
701
|
+
extra=extra_with_env,
|
|
702
|
+
probe=probe,
|
|
703
|
+
)
|
|
704
|
+
result.requested = AUTO_PROVIDER
|
|
705
|
+
result.detected = detection
|
|
706
|
+
return result
|
|
707
|
+
|
|
385
708
|
if requested in {"hash", "local", "fallback", ""}:
|
|
386
709
|
hash_prov = HashEmbeddingProvider(dim=int(dim or DEFAULT_EMBEDDING_DIM))
|
|
387
710
|
return ResolvedEmbedder(
|
|
@@ -84,6 +84,7 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
84
84
|
{"id": "anthropic/claude-opus-4.7", "name": "Claude Opus 4.7 via OpenRouter", "family": "Claude"},
|
|
85
85
|
{"id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6 via OpenRouter", "family": "Claude"},
|
|
86
86
|
{"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
|
|
87
|
+
{"id": "qwen/qwen3.8-27b", "name": "Qwen3.8 27B via OpenRouter", "family": "Qwen"},
|
|
87
88
|
{"id": "qwen/qwen3.6-35b-a3b", "name": "Qwen3.6 35B A3B via OpenRouter", "family": "Qwen"},
|
|
88
89
|
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
|
|
89
90
|
{"id": "x-ai/grok-4.6", "name": "Grok 4.6 via OpenRouter", "family": "Grok"},
|
|
@@ -94,6 +95,7 @@ PROVIDER_MODEL_CATALOG = {
|
|
|
94
95
|
{"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout", "family": "Llama"},
|
|
95
96
|
],
|
|
96
97
|
"together": [
|
|
98
|
+
{"id": "Qwen/Qwen3.8-27B", "name": "Qwen3.8 27B", "family": "Qwen"},
|
|
97
99
|
{"id": "Qwen/Qwen3.6-27B", "name": "Qwen3.6 27B", "family": "Qwen"},
|
|
98
100
|
{"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B", "family": "Gemma"},
|
|
99
101
|
{"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
|