ltcai 11.1.0 → 11.4.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 +47 -54
- package/docs/CHANGELOG.md +94 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/portability/sharing.py +714 -0
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +152 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +2 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +44 -2
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1377
- package/lattice_brain/portability.py +0 -1210
- package/latticeai/core/agent.py +0 -1412
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1422
- package/latticeai/services/brain_intelligence.py +0 -967
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1277
- package/static/app/assets/Act-D0HWqtn0.js +0 -1
- package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
- package/static/app/assets/Capture-CT8v1StE.js +0 -1
- package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
- package/static/app/assets/Library-DDoxFE5c.js +0 -1
- package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
- package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
- package/static/app/assets/System-BRllvYXd.js +0 -1
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/index-0AvoEBzJ.js +0 -10
- package/static/app/assets/input-B_5ZJ9oy.js +0 -1
- package/static/app/assets/primitives-CVwew78r.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
- package/static/app/assets/utils-DcDMoZIe.js +0 -4
- package/static/app/assets/workspace-LtRRSKTf.js +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Model identity, engine readiness, and the load entrypoints.
|
|
2
|
+
|
|
3
|
+
The path from a string a user clicked to a loaded model: resolve engine
|
|
4
|
+
aliases into one canonical identity, make sure the engine that identity needs
|
|
5
|
+
is installed, then hand off to ``latticeai.services.model_loading`` for the
|
|
6
|
+
load itself (blocking and streaming forms) and to
|
|
7
|
+
``latticeai.services.model_engines`` for the post-load smoke test.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, AsyncIterator, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from latticeai.core.model_resolution import ModelResolution as _ModelResolution
|
|
16
|
+
from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS, ensure_mlx_runtime
|
|
17
|
+
from latticeai.services.model_catalog import ENGINE_INSTALLERS, MODEL_ENGINE_ALIASES
|
|
18
|
+
from latticeai.services.model_errors import ModelRuntimeError
|
|
19
|
+
from latticeai.services.model_runtime.engines import (
|
|
20
|
+
engine_installed,
|
|
21
|
+
engine_support_status,
|
|
22
|
+
)
|
|
23
|
+
from latticeai.services.model_runtime.state import ModelRuntimeState
|
|
24
|
+
from latticeai.services.model_runtime.status import install_engine
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_model_alias(model_id: str, engine: Optional[str] = None) -> str:
|
|
28
|
+
raw = model_id.strip()
|
|
29
|
+
engine_hint = (engine or "").strip().lower()
|
|
30
|
+
provider: Optional[str] = None
|
|
31
|
+
model_name = raw
|
|
32
|
+
if ":" in raw:
|
|
33
|
+
prefix, rest = raw.split(":", 1)
|
|
34
|
+
prefix = prefix.strip().lower()
|
|
35
|
+
if prefix in {"ollama", "vllm", "lmstudio", "llamacpp", "local_mlx", "mlx"}:
|
|
36
|
+
provider = "local_mlx" if prefix in {"local_mlx", "mlx"} else prefix
|
|
37
|
+
model_name = rest.strip()
|
|
38
|
+
provider = provider or ("local_mlx" if engine_hint in {"", "local_mlx", "mlx"} else engine_hint)
|
|
39
|
+
aliases = MODEL_ENGINE_ALIASES.get(model_name.lower())
|
|
40
|
+
if not aliases:
|
|
41
|
+
return raw
|
|
42
|
+
mapped = aliases.get(provider)
|
|
43
|
+
if not mapped:
|
|
44
|
+
return raw
|
|
45
|
+
return mapped if provider == "local_mlx" else f"{provider}:{mapped}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_local_model_request(model_id: str, engine: Optional[str] = None) -> str:
|
|
49
|
+
model_id = _resolve_model_alias(model_id, engine)
|
|
50
|
+
engine = (engine or "").strip().lower()
|
|
51
|
+
if engine in {"local_mlx", "mlx"} and model_id.startswith(("local_mlx:", "mlx:")):
|
|
52
|
+
return model_id.split(":", 1)[1].strip()
|
|
53
|
+
if engine and engine not in {"local_mlx", "mlx"} and ":" not in model_id:
|
|
54
|
+
return f"{engine}:{model_id}"
|
|
55
|
+
return model_id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ensure_engine_ready(engine: str, *, state: ModelRuntimeState) -> Dict[str, Any]:
|
|
59
|
+
engine = "local_mlx" if engine == "mlx" else engine
|
|
60
|
+
if engine not in ENGINE_INSTALLERS and engine not in OPENAI_COMPATIBLE_PROVIDERS:
|
|
61
|
+
raise ModelRuntimeError(status_code=400, detail=f"지원하지 않는 엔진입니다: {engine}")
|
|
62
|
+
support = engine_support_status(engine)
|
|
63
|
+
if not support["supported"]:
|
|
64
|
+
raise ModelRuntimeError(status_code=400, detail=str(support["reason"]))
|
|
65
|
+
|
|
66
|
+
if engine_installed(engine):
|
|
67
|
+
if engine == "local_mlx":
|
|
68
|
+
ensure_mlx_runtime()
|
|
69
|
+
return {"engine": engine, "installed": True, "installed_now": False}
|
|
70
|
+
|
|
71
|
+
if engine not in ENGINE_INSTALLERS:
|
|
72
|
+
raise ModelRuntimeError(status_code=400, detail=f"{engine} 엔진 설치 방법이 등록되어 있지 않습니다.")
|
|
73
|
+
|
|
74
|
+
result = install_engine(engine, state=state)
|
|
75
|
+
if result.get("returncode") not in (0, None) or not engine_installed(engine):
|
|
76
|
+
detail = result.get("stderr") or result.get("stdout") or f"{engine} 설치에 실패했습니다."
|
|
77
|
+
raise ModelRuntimeError(status_code=500, detail=str(detail)[-2000:])
|
|
78
|
+
|
|
79
|
+
if engine == "local_mlx":
|
|
80
|
+
ensure_mlx_runtime()
|
|
81
|
+
return {"engine": engine, "installed": True, "installed_now": True, "install": result}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_model_resolution(
|
|
85
|
+
input_id: str,
|
|
86
|
+
engine: Optional[str],
|
|
87
|
+
*,
|
|
88
|
+
user_email: Optional[str] = None,
|
|
89
|
+
display_name: Optional[str] = None,
|
|
90
|
+
) -> _ModelResolution:
|
|
91
|
+
"""피드백 #1/#2 공용 ModelResolution 생성기.
|
|
92
|
+
|
|
93
|
+
사용자가 클릭한 input_id + engine 힌트를 받아 모든 단계가 공유할
|
|
94
|
+
canonical identity를 만든다.
|
|
95
|
+
"""
|
|
96
|
+
normalized = normalize_local_model_request(input_id, engine)
|
|
97
|
+
return _ModelResolution.from_request(
|
|
98
|
+
normalized,
|
|
99
|
+
engine=engine,
|
|
100
|
+
user_email=user_email,
|
|
101
|
+
display_name=display_name or input_id,
|
|
102
|
+
engine_aliases=MODEL_ENGINE_ALIASES,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_LOCAL_SMOKE_ENGINES = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _smoke_test_loaded_model(
|
|
110
|
+
resolution: _ModelResolution,
|
|
111
|
+
*,
|
|
112
|
+
api_key_override: Optional[str] = None,
|
|
113
|
+
state: ModelRuntimeState,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
# Delegated to model_engines for server decomp
|
|
116
|
+
# Absolute since v11.3.0: this file moved one level down into the
|
|
117
|
+
# model_runtime package, so ``.model_engines`` would resolve inside it.
|
|
118
|
+
from latticeai.services.model_engines import (
|
|
119
|
+
_smoke_test_loaded_model as _impl_smoke,
|
|
120
|
+
)
|
|
121
|
+
return await _impl_smoke(
|
|
122
|
+
resolution,
|
|
123
|
+
api_key_override=api_key_override,
|
|
124
|
+
model_router=state.router,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def prepare_and_load_model(
|
|
129
|
+
model_id: str,
|
|
130
|
+
request: Any,
|
|
131
|
+
engine: Optional[str] = None,
|
|
132
|
+
user_email: Optional[str] = None,
|
|
133
|
+
adapter_path: Optional[str] = None,
|
|
134
|
+
draft_model_id: Optional[str] = None,
|
|
135
|
+
allow_download: bool = False,
|
|
136
|
+
*,
|
|
137
|
+
state: ModelRuntimeState,
|
|
138
|
+
) -> Dict[str, Any]:
|
|
139
|
+
from latticeai.services.model_loading import prepare_and_load_model as _impl
|
|
140
|
+
|
|
141
|
+
return await _impl(
|
|
142
|
+
model_id,
|
|
143
|
+
request,
|
|
144
|
+
engine=engine,
|
|
145
|
+
user_email=user_email,
|
|
146
|
+
adapter_path=adapter_path,
|
|
147
|
+
draft_model_id=draft_model_id,
|
|
148
|
+
allow_download=allow_download,
|
|
149
|
+
runtime_state=state,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def sse_event(event: str, data: Dict[str, Any]) -> str:
|
|
154
|
+
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def prepare_and_load_model_stream(
|
|
158
|
+
model_id: str,
|
|
159
|
+
request: Any,
|
|
160
|
+
engine: Optional[str] = None,
|
|
161
|
+
user_email: Optional[str] = None,
|
|
162
|
+
allow_download: bool = False,
|
|
163
|
+
*,
|
|
164
|
+
state: ModelRuntimeState,
|
|
165
|
+
) -> AsyncIterator[str]:
|
|
166
|
+
from latticeai.services.model_loading import (
|
|
167
|
+
prepare_and_load_model_stream as _impl,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async for event in _impl(
|
|
171
|
+
model_id,
|
|
172
|
+
request,
|
|
173
|
+
engine=engine,
|
|
174
|
+
user_email=user_email,
|
|
175
|
+
allow_download=allow_download,
|
|
176
|
+
runtime_state=state,
|
|
177
|
+
):
|
|
178
|
+
yield event
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The bound runtime service — one application's model operations.
|
|
2
|
+
|
|
3
|
+
Everything above is a free function taking an explicit ``state``. This is the
|
|
4
|
+
object the composition root holds: it carries that state plus the only piece of
|
|
5
|
+
per-application operational data (the cloud verification cache), so a second
|
|
6
|
+
ASGI app in the same process starts with an empty cache rather than inheriting
|
|
7
|
+
probe results it never ran.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from latticeai.services.model_runtime.cloud import verify_cloud_models
|
|
16
|
+
from latticeai.services.model_runtime.loading import (
|
|
17
|
+
prepare_and_load_model,
|
|
18
|
+
prepare_and_load_model_stream,
|
|
19
|
+
)
|
|
20
|
+
from latticeai.services.model_runtime.state import (
|
|
21
|
+
ModelRuntimeState,
|
|
22
|
+
create_model_runtime_state,
|
|
23
|
+
)
|
|
24
|
+
from latticeai.services.model_runtime.status import (
|
|
25
|
+
engine_status,
|
|
26
|
+
install_engine,
|
|
27
|
+
runtime_features,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def configure_model_runtime(**deps: Any) -> "ModelRuntimeService":
|
|
32
|
+
"""Compatibility factory returning an isolated, bound runtime service.
|
|
33
|
+
|
|
34
|
+
The historical function mutated process-wide module globals. Keeping the
|
|
35
|
+
import path while returning a service preserves practical construction
|
|
36
|
+
compatibility without ambient state or cross-application leakage.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return ModelRuntimeService(create_model_runtime_state(**deps))
|
|
40
|
+
|
|
41
|
+
@dataclass(slots=True)
|
|
42
|
+
class ModelRuntimeService:
|
|
43
|
+
"""Bound model operations for one explicitly configured application.
|
|
44
|
+
|
|
45
|
+
All configuration and app-owned callables live on ``state``. Operational
|
|
46
|
+
verification cache data belongs to this service instance, so creating a
|
|
47
|
+
second ASGI app cannot inherit credentials, routers, or probe results from
|
|
48
|
+
the first one.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
state: ModelRuntimeState
|
|
52
|
+
_cloud_verify_cache: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def runtime_features(self) -> Dict[str, Any]:
|
|
55
|
+
return runtime_features(state=self.state)
|
|
56
|
+
|
|
57
|
+
def engine_status(self) -> List[Dict[str, Any]]:
|
|
58
|
+
return engine_status(
|
|
59
|
+
state=self.state,
|
|
60
|
+
cloud_verify_cache=self._cloud_verify_cache,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def install_engine(
|
|
64
|
+
self,
|
|
65
|
+
engine: str,
|
|
66
|
+
confirmation_token: Optional[str] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
return install_engine(
|
|
69
|
+
engine,
|
|
70
|
+
confirmation_token=confirmation_token,
|
|
71
|
+
state=self.state,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def verify_cloud_models(
|
|
75
|
+
self,
|
|
76
|
+
force: bool = False,
|
|
77
|
+
provider_filter: Optional[str] = None,
|
|
78
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
79
|
+
return await verify_cloud_models(
|
|
80
|
+
force=force,
|
|
81
|
+
provider_filter=provider_filter,
|
|
82
|
+
state=self.state,
|
|
83
|
+
cache=self._cloud_verify_cache,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def prepare_and_load_model(
|
|
87
|
+
self,
|
|
88
|
+
model_id: str,
|
|
89
|
+
request: Any,
|
|
90
|
+
engine: Optional[str] = None,
|
|
91
|
+
user_email: Optional[str] = None,
|
|
92
|
+
adapter_path: Optional[str] = None,
|
|
93
|
+
draft_model_id: Optional[str] = None,
|
|
94
|
+
allow_download: bool = False,
|
|
95
|
+
) -> Dict[str, Any]:
|
|
96
|
+
return await prepare_and_load_model(
|
|
97
|
+
model_id,
|
|
98
|
+
request,
|
|
99
|
+
engine=engine,
|
|
100
|
+
user_email=user_email,
|
|
101
|
+
adapter_path=adapter_path,
|
|
102
|
+
draft_model_id=draft_model_id,
|
|
103
|
+
allow_download=allow_download,
|
|
104
|
+
state=self.state,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def prepare_and_load_model_stream(
|
|
108
|
+
self,
|
|
109
|
+
model_id: str,
|
|
110
|
+
request: Any,
|
|
111
|
+
engine: Optional[str] = None,
|
|
112
|
+
user_email: Optional[str] = None,
|
|
113
|
+
allow_download: bool = False,
|
|
114
|
+
) -> AsyncIterator[str]:
|
|
115
|
+
async for event in prepare_and_load_model_stream(
|
|
116
|
+
model_id,
|
|
117
|
+
request,
|
|
118
|
+
engine=engine,
|
|
119
|
+
user_email=user_email,
|
|
120
|
+
allow_download=allow_download,
|
|
121
|
+
state=self.state,
|
|
122
|
+
):
|
|
123
|
+
yield event
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_model_runtime(**deps: Any) -> ModelRuntimeService:
|
|
127
|
+
"""Build the application's isolated model runtime service."""
|
|
128
|
+
|
|
129
|
+
return ModelRuntimeService(create_model_runtime_state(**deps))
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""The immutable dependency set one model runtime is configured with.
|
|
2
|
+
|
|
3
|
+
``ModelRuntimeState`` replaced the process-wide module ``STATE`` object: every
|
|
4
|
+
value the runtime needs — router, mode, paths, download/auth policy, the
|
|
5
|
+
app-owned callables — is passed in explicitly and never mutated afterwards, so
|
|
6
|
+
a second ASGI application in the same interpreter cannot inherit the first
|
|
7
|
+
one's credentials or configuration.
|
|
8
|
+
|
|
9
|
+
Also home to the two consent gates (:func:`_download_block`,
|
|
10
|
+
:func:`_engine_install_block`): presence of a token or a model name never
|
|
11
|
+
authorises network or installer activity on its own.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field, fields
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Optional
|
|
19
|
+
|
|
20
|
+
from latticeai.core.model_compat import (
|
|
21
|
+
SMOKE_PROMPT as _SMOKE_PROMPT,
|
|
22
|
+
)
|
|
23
|
+
from latticeai.core.model_compat import (
|
|
24
|
+
friendly_model_runtime_error as _friendly_model_runtime_error,
|
|
25
|
+
)
|
|
26
|
+
from latticeai.core.model_compat import (
|
|
27
|
+
model_runtime_compatibility as _model_runtime_compatibility,
|
|
28
|
+
)
|
|
29
|
+
from latticeai.services.model_errors import ModelRuntimeError
|
|
30
|
+
|
|
31
|
+
# ``model_loading._get_model_runtime_deps`` imports these private names from
|
|
32
|
+
# this module to preserve the historical model_runtime wiring surface.
|
|
33
|
+
_MODEL_LOADING_COMPAT_EXPORTS = (
|
|
34
|
+
_friendly_model_runtime_error,
|
|
35
|
+
_model_runtime_compatibility,
|
|
36
|
+
_SMOKE_PROMPT,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _missing_current_user(_request: Any) -> Optional[str]:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _missing_user_api_key(_email: Optional[str], _provider: str) -> Optional[str]:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class ModelRuntimeState:
|
|
50
|
+
"""Immutable application-owned dependencies for one model runtime.
|
|
51
|
+
|
|
52
|
+
Upper-case configuration field names intentionally match the long-standing
|
|
53
|
+
composition-root vocabulary. Unlike the former module ``STATE`` object,
|
|
54
|
+
instances are explicit, immutable, and safe to create more than once in a
|
|
55
|
+
process (for example in isolated tests or multiple ASGI applications).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
router: Any = None
|
|
59
|
+
APP_MODE: str = "local"
|
|
60
|
+
DEFAULT_HOST: str = "127.0.0.1"
|
|
61
|
+
DEFAULT_PORT: int = 4825
|
|
62
|
+
DATA_DIR: Path = field(default_factory=lambda: Path.home() / ".latticeai")
|
|
63
|
+
BASE_DIR: Path = field(default_factory=Path.cwd)
|
|
64
|
+
ENABLE_TELEGRAM: bool = False
|
|
65
|
+
ENABLE_GRAPH: bool = True
|
|
66
|
+
AUTOLOAD_MODELS: bool = False
|
|
67
|
+
MODEL_IDLE_UNLOAD_SECONDS: int = 0
|
|
68
|
+
ALLOW_MODEL_DOWNLOADS: bool = False
|
|
69
|
+
MODEL_DOWNLOAD_TIMEOUT: int = 300
|
|
70
|
+
ALLOW_LOCAL_MODELS: bool = True
|
|
71
|
+
REQUIRE_AUTH: bool = False
|
|
72
|
+
INVITE_GATE_ENABLED: bool = False
|
|
73
|
+
ALLOW_PLAINTEXT_API_KEYS: bool = False
|
|
74
|
+
CORS_ALLOW_NETWORK: bool = False
|
|
75
|
+
PUBLIC_MODEL: str = "openai:gpt-4o-mini"
|
|
76
|
+
LOCAL_MODEL: str = "mlx-community/gemma-4-12B-it-4bit"
|
|
77
|
+
IS_PUBLIC_MODE: bool = False
|
|
78
|
+
keyring: Any = None
|
|
79
|
+
get_current_user: Callable[[Any], Optional[str]] = _missing_current_user
|
|
80
|
+
get_user_api_key: Callable[[Optional[str], str], Optional[str]] = _missing_user_api_key
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_model_runtime_state(**deps: Any) -> ModelRuntimeState:
|
|
84
|
+
"""Create an immutable runtime dependency set with strict key validation."""
|
|
85
|
+
|
|
86
|
+
known = {item.name for item in fields(ModelRuntimeState)}
|
|
87
|
+
unknown = sorted(set(deps) - known)
|
|
88
|
+
if unknown:
|
|
89
|
+
raise TypeError(f"unknown model runtime dependencies: {', '.join(unknown)}")
|
|
90
|
+
return ModelRuntimeState(**deps)
|
|
91
|
+
|
|
92
|
+
def _download_allowed(
|
|
93
|
+
allow_download: bool = False, *, state: ModelRuntimeState
|
|
94
|
+
) -> bool:
|
|
95
|
+
autoload = state.AUTOLOAD_MODELS
|
|
96
|
+
configured = state.ALLOW_MODEL_DOWNLOADS
|
|
97
|
+
return bool(allow_download) or bool(configured) or bool(autoload)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _download_block(provider: str, model_name: str) -> None:
|
|
101
|
+
raise ModelRuntimeError(
|
|
102
|
+
status_code=409,
|
|
103
|
+
detail={
|
|
104
|
+
"status": "unavailable",
|
|
105
|
+
"capability": "model_download",
|
|
106
|
+
"provider": provider,
|
|
107
|
+
"model": model_name,
|
|
108
|
+
"reason": (
|
|
109
|
+
"Model files are not present locally. Lattice AI does not start "
|
|
110
|
+
"outbound model downloads by default, and token/model presence "
|
|
111
|
+
"alone never authorizes network activity."
|
|
112
|
+
),
|
|
113
|
+
"action": "Use the explicit pull/prepare flow with download consent, or set LATTICEAI_ALLOW_MODEL_DOWNLOADS=true.",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _engine_install_block(engine: str) -> None:
|
|
119
|
+
raise ModelRuntimeError(
|
|
120
|
+
status_code=409,
|
|
121
|
+
detail={
|
|
122
|
+
"status": "unavailable",
|
|
123
|
+
"capability": "engine_install",
|
|
124
|
+
"engine": engine,
|
|
125
|
+
"reason": (
|
|
126
|
+
"The requested local runtime is not installed. Lattice AI does not "
|
|
127
|
+
"run package-manager or installer commands from Model Load by default."
|
|
128
|
+
),
|
|
129
|
+
"action": "Install the runtime explicitly from Library/System setup, or enable explicit download/install consent for this request.",
|
|
130
|
+
},
|
|
131
|
+
)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""What this machine can run: the engine/model status payload.
|
|
2
|
+
|
|
3
|
+
:func:`engine_status` is the single answer the Library screen renders — every
|
|
4
|
+
engine (local, local-server, cloud), whether it is installed, whether its
|
|
5
|
+
install is even supported here, and which catalogue models are already pulled.
|
|
6
|
+
:func:`runtime_features` is the same idea for the application itself, and
|
|
7
|
+
:func:`install_engine` is the one write this module owns.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from latticeai.models.router import HF_MODELS_ROOT
|
|
15
|
+
from latticeai.services.model_catalog import (
|
|
16
|
+
ENGINE_INSTALLERS,
|
|
17
|
+
ENGINE_MODEL_CATALOG,
|
|
18
|
+
filter_lower_family_versions,
|
|
19
|
+
)
|
|
20
|
+
from latticeai.services.model_engines import (
|
|
21
|
+
install_engine as _install_engine,
|
|
22
|
+
)
|
|
23
|
+
from latticeai.services.model_runtime.download import hf_model_ready
|
|
24
|
+
from latticeai.services.model_runtime.engines import (
|
|
25
|
+
_safe_engine_install_plan,
|
|
26
|
+
engine_installed,
|
|
27
|
+
engine_support_status,
|
|
28
|
+
get_lmstudio_models,
|
|
29
|
+
get_ollama_pulled_models,
|
|
30
|
+
)
|
|
31
|
+
from latticeai.services.model_runtime.state import ModelRuntimeState
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def engine_status(
|
|
35
|
+
*,
|
|
36
|
+
state: ModelRuntimeState,
|
|
37
|
+
cloud_verify_cache: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
38
|
+
) -> List[Dict]:
|
|
39
|
+
r = state.router
|
|
40
|
+
verify_cache = cloud_verify_cache or {}
|
|
41
|
+
cloud_models = r.detected_cloud_models() if r else []
|
|
42
|
+
cloud_by_provider: Dict[str, List[Dict[str, Any]]] = {}
|
|
43
|
+
for model in cloud_models:
|
|
44
|
+
cloud_by_provider.setdefault(model["provider"], []).append(model)
|
|
45
|
+
|
|
46
|
+
ollama_installed = engine_installed("ollama")
|
|
47
|
+
pulled = get_ollama_pulled_models() if ollama_installed else set()
|
|
48
|
+
ollama_models = []
|
|
49
|
+
for m in ENGINE_MODEL_CATALOG["ollama"]:
|
|
50
|
+
pull_name = m["id"].removeprefix("ollama:")
|
|
51
|
+
ollama_models.append({**m, "pulled": pull_name in pulled})
|
|
52
|
+
ollama_models = filter_lower_family_versions(ollama_models)
|
|
53
|
+
|
|
54
|
+
HF_MODELS_ROOT.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
mlx_models = []
|
|
56
|
+
for m in ENGINE_MODEL_CATALOG.get("local_mlx", []):
|
|
57
|
+
repo_id = m["id"]
|
|
58
|
+
mlx_models.append({**m, "pulled": hf_model_ready(repo_id, "local_mlx")})
|
|
59
|
+
mlx_models = filter_lower_family_versions(mlx_models)
|
|
60
|
+
|
|
61
|
+
vllm_models = []
|
|
62
|
+
for m in ENGINE_MODEL_CATALOG.get("vllm", []):
|
|
63
|
+
repo_id = m["id"].removeprefix("vllm:")
|
|
64
|
+
vllm_models.append({**m, "pulled": hf_model_ready(repo_id, "vllm")})
|
|
65
|
+
vllm_models = filter_lower_family_versions(vllm_models)
|
|
66
|
+
|
|
67
|
+
lmstudio_models = []
|
|
68
|
+
downloaded_lmstudio = get_lmstudio_models()
|
|
69
|
+
downloaded_by_key: Dict[str, Dict[str, Any]] = {}
|
|
70
|
+
for item in downloaded_lmstudio:
|
|
71
|
+
key = str(item.get("key") or "").strip()
|
|
72
|
+
if not key:
|
|
73
|
+
continue
|
|
74
|
+
downloaded_by_key[key] = item
|
|
75
|
+
loaded_instances = item.get("loaded_instances") or []
|
|
76
|
+
lmstudio_models.append({
|
|
77
|
+
"id": f"lmstudio:{key}",
|
|
78
|
+
"name": item.get("display_name") or f"LM Studio · {key}",
|
|
79
|
+
"family": item.get("architecture") or item.get("publisher") or "LM Studio",
|
|
80
|
+
"tag": "loaded-server-model" if loaded_instances else "downloaded",
|
|
81
|
+
"size": item.get("params_string") or item.get("format") or "LM Studio",
|
|
82
|
+
"pullable": True,
|
|
83
|
+
"pulled": True,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if not lmstudio_models:
|
|
87
|
+
for m in ENGINE_MODEL_CATALOG.get("lmstudio", []):
|
|
88
|
+
lmstudio_models.append({**m, "pulled": False})
|
|
89
|
+
else:
|
|
90
|
+
known_ids = {item["id"] for item in lmstudio_models}
|
|
91
|
+
for m in ENGINE_MODEL_CATALOG.get("lmstudio", []):
|
|
92
|
+
repo_id = m["id"].removeprefix("lmstudio:")
|
|
93
|
+
if f"lmstudio:{repo_id}" not in known_ids and repo_id not in downloaded_by_key:
|
|
94
|
+
lmstudio_models.append({**m, "pulled": False})
|
|
95
|
+
lmstudio_models = filter_lower_family_versions(lmstudio_models)
|
|
96
|
+
|
|
97
|
+
llamacpp_models = []
|
|
98
|
+
for m in ENGINE_MODEL_CATALOG.get("llamacpp", []):
|
|
99
|
+
repo_id = m["id"].removeprefix("llamacpp:")
|
|
100
|
+
llamacpp_models.append({**m, "pulled": hf_model_ready(repo_id, "llamacpp")})
|
|
101
|
+
llamacpp_models = filter_lower_family_versions(llamacpp_models)
|
|
102
|
+
|
|
103
|
+
local_server_specs: List[Dict[str, Any]] = [
|
|
104
|
+
{
|
|
105
|
+
"id": "vllm",
|
|
106
|
+
"name": "vLLM",
|
|
107
|
+
"description": "vLLM OpenAI 호환 서버(예: http://localhost:8000/v1)에 연결합니다.",
|
|
108
|
+
"requires": "VLLM_BASE_URL",
|
|
109
|
+
"note": engine_support_status("vllm").get("reason"),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "lmstudio",
|
|
113
|
+
"name": "LM Studio",
|
|
114
|
+
"description": "LM Studio 로컬 OpenAI 호환 서버에 연결합니다.",
|
|
115
|
+
"requires": "LMSTUDIO_BASE_URL",
|
|
116
|
+
"note": (
|
|
117
|
+
"다운로드된 모델은 자동 감지하고, 선택 시 필요하면 다운로드 후 바로 로드합니다."
|
|
118
|
+
if downloaded_lmstudio else
|
|
119
|
+
"LM Studio 설치 후 모델을 선택하면 Local Server 시작, 다운로드, 로드를 자동으로 진행합니다."
|
|
120
|
+
),
|
|
121
|
+
"server_ready": bool(downloaded_lmstudio),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "llamacpp",
|
|
125
|
+
"name": "llama.cpp",
|
|
126
|
+
"description": "llama.cpp 서버(OpenAI 호환 /v1)에 연결합니다.",
|
|
127
|
+
"requires": "LLAMACPP_BASE_URL",
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
engines = [
|
|
132
|
+
{
|
|
133
|
+
"id": "local_mlx",
|
|
134
|
+
"name": "MLX",
|
|
135
|
+
"kind": "local",
|
|
136
|
+
"description": "Apple Silicon GPU에서 MLX-VLM 모델을 직접 실행하고, Gemma 4는 필요 시 MLX-LM 텍스트 경로로 재시도합니다.",
|
|
137
|
+
"installed": engine_installed("local_mlx"),
|
|
138
|
+
"installable": True,
|
|
139
|
+
"install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
|
|
140
|
+
"install_plan": _safe_engine_install_plan("local_mlx", base_dir=state.BASE_DIR),
|
|
141
|
+
"models": mlx_models,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "ollama",
|
|
145
|
+
"name": "Ollama",
|
|
146
|
+
"kind": "local-server",
|
|
147
|
+
"description": "Ollama 로컬 서버를 OpenAI 호환 엔진처럼 사용합니다.",
|
|
148
|
+
"installed": ollama_installed,
|
|
149
|
+
"installable": True,
|
|
150
|
+
"install_label": ENGINE_INSTALLERS["ollama"]["label"],
|
|
151
|
+
"install_plan": _safe_engine_install_plan("ollama", base_dir=state.BASE_DIR),
|
|
152
|
+
"models": ollama_models,
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
for spec in local_server_specs:
|
|
156
|
+
support = engine_support_status(spec["id"])
|
|
157
|
+
engines.append({
|
|
158
|
+
"id": spec["id"],
|
|
159
|
+
"name": spec["name"],
|
|
160
|
+
"kind": "local-server",
|
|
161
|
+
"description": spec["description"],
|
|
162
|
+
"installed": engine_installed(spec["id"]),
|
|
163
|
+
"supported": support["supported"],
|
|
164
|
+
"support_reason": support["reason"],
|
|
165
|
+
"installable": support["supported"] and spec["id"] in ENGINE_INSTALLERS,
|
|
166
|
+
"install_label": ENGINE_INSTALLERS.get(spec["id"], {}).get("label"),
|
|
167
|
+
"install_plan": (
|
|
168
|
+
_safe_engine_install_plan(spec["id"], base_dir=state.BASE_DIR)
|
|
169
|
+
if spec["id"] in ENGINE_INSTALLERS
|
|
170
|
+
else None
|
|
171
|
+
),
|
|
172
|
+
"requires": spec["requires"],
|
|
173
|
+
"models": (
|
|
174
|
+
vllm_models if spec["id"] == "vllm"
|
|
175
|
+
else lmstudio_models if spec["id"] == "lmstudio"
|
|
176
|
+
else llamacpp_models if spec["id"] == "llamacpp"
|
|
177
|
+
else ENGINE_MODEL_CATALOG.get(spec["id"], [])
|
|
178
|
+
),
|
|
179
|
+
"note": spec.get("note") or support["reason"] or f"{spec['requires']} 설정 시 활성화됩니다.",
|
|
180
|
+
"server_ready": spec.get("server_ready"),
|
|
181
|
+
})
|
|
182
|
+
for provider in ["openai", "openrouter", "groq", "together", "xai"]:
|
|
183
|
+
env_key = next((item.get("requires") for item in cloud_by_provider.get(provider, []) if item.get("requires")), None)
|
|
184
|
+
provider_models = []
|
|
185
|
+
for model in cloud_by_provider.get(provider, []):
|
|
186
|
+
cache = verify_cache.get(str(model.get("id") or ""))
|
|
187
|
+
provider_models.append({
|
|
188
|
+
**model,
|
|
189
|
+
"verified": cache.get("ok") if cache else None,
|
|
190
|
+
"verify_reason": cache.get("reason") if cache else None,
|
|
191
|
+
})
|
|
192
|
+
engines.append({
|
|
193
|
+
"id": provider,
|
|
194
|
+
"name": provider.title(),
|
|
195
|
+
"kind": "cloud",
|
|
196
|
+
"description": "OpenAI 호환 Chat Completions API로 cloud LLM을 실행합니다.",
|
|
197
|
+
"installed": engine_installed(provider),
|
|
198
|
+
"installable": True,
|
|
199
|
+
"install_label": ENGINE_INSTALLERS[provider]["label"],
|
|
200
|
+
"install_plan": _safe_engine_install_plan(provider, base_dir=state.BASE_DIR),
|
|
201
|
+
"requires": env_key,
|
|
202
|
+
"models": provider_models,
|
|
203
|
+
})
|
|
204
|
+
return engines
|
|
205
|
+
|
|
206
|
+
def runtime_features(*, state: ModelRuntimeState) -> Dict:
|
|
207
|
+
s = state
|
|
208
|
+
r = s.router
|
|
209
|
+
return {
|
|
210
|
+
"mode": s.APP_MODE,
|
|
211
|
+
"public": s.IS_PUBLIC_MODE,
|
|
212
|
+
"host": s.DEFAULT_HOST,
|
|
213
|
+
"port": s.DEFAULT_PORT,
|
|
214
|
+
"data_dir": str(s.DATA_DIR),
|
|
215
|
+
"telegram_enabled": s.ENABLE_TELEGRAM,
|
|
216
|
+
"graph_enabled": s.ENABLE_GRAPH,
|
|
217
|
+
"autoload_models": s.AUTOLOAD_MODELS,
|
|
218
|
+
"model_idle_unload_seconds": s.MODEL_IDLE_UNLOAD_SECONDS,
|
|
219
|
+
"allow_model_downloads": s.ALLOW_MODEL_DOWNLOADS,
|
|
220
|
+
"model_download_timeout": s.MODEL_DOWNLOAD_TIMEOUT,
|
|
221
|
+
"model_memory_policy": r.model_memory_policy() if r else None,
|
|
222
|
+
"allow_local_models": s.ALLOW_LOCAL_MODELS,
|
|
223
|
+
"security": {
|
|
224
|
+
"host": s.DEFAULT_HOST,
|
|
225
|
+
"require_auth": s.REQUIRE_AUTH,
|
|
226
|
+
"invite_gate_enabled": s.INVITE_GATE_ENABLED,
|
|
227
|
+
"keyring_available": s.keyring is not None,
|
|
228
|
+
"plaintext_api_keys_allowed": s.ALLOW_PLAINTEXT_API_KEYS,
|
|
229
|
+
"cors_allow_network": s.CORS_ALLOW_NETWORK,
|
|
230
|
+
},
|
|
231
|
+
"default_model": s.PUBLIC_MODEL if s.IS_PUBLIC_MODE else s.LOCAL_MODEL,
|
|
232
|
+
"local_only_features": {
|
|
233
|
+
"mlx": s.ALLOW_LOCAL_MODELS and not s.IS_PUBLIC_MODE,
|
|
234
|
+
"telegram_bridge": s.ENABLE_TELEGRAM,
|
|
235
|
+
"desktop_chrome_bridge": not s.IS_PUBLIC_MODE,
|
|
236
|
+
"computer_use_bridge": not s.IS_PUBLIC_MODE,
|
|
237
|
+
},
|
|
238
|
+
"public_features": {
|
|
239
|
+
"web_ui": True,
|
|
240
|
+
"openai_compatible_models": True,
|
|
241
|
+
"persistent_data_dir": str(s.DATA_DIR),
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def install_engine(
|
|
246
|
+
engine: str,
|
|
247
|
+
confirmation_token: Optional[str] = None,
|
|
248
|
+
*,
|
|
249
|
+
state: ModelRuntimeState,
|
|
250
|
+
) -> Dict:
|
|
251
|
+
return _install_engine(
|
|
252
|
+
engine,
|
|
253
|
+
confirmation_token=confirmation_token,
|
|
254
|
+
base_dir=state.BASE_DIR,
|
|
255
|
+
)
|