ltcai 11.2.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.
Files changed (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,341 @@
1
+ """Local engine discovery, server hand-off, and the LM Studio HTTP client.
2
+
3
+ Thin wrappers over ``latticeai.services.model_engines`` that keep the historical
4
+ ``model_runtime`` import path alive, plus the one piece of real logic that never
5
+ belonged in the engine layer: the LM Studio native API (list / download / load)
6
+ and its short-lived model cache.
7
+
8
+ ``_LMSTUDIO_MODELS_CACHE`` and ``_LMSTUDIO_MODELS_CACHE_TS`` are rebindable
9
+ module state and therefore live here and nowhere else — the package
10
+ ``__init__`` deliberately does not re-export them, because a
11
+ ``from … import`` copy would freeze at import time and quietly disagree with
12
+ the live value.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import json
19
+ import os
20
+ import shutil
21
+ import time
22
+ import urllib.error
23
+ import urllib.request
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS, AsyncOpenAI
28
+ from latticeai.services.model_engines import (
29
+ LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
30
+ )
31
+ from latticeai.services.model_engines import (
32
+ engine_install_plan as _engine_install_plan,
33
+ )
34
+ from latticeai.services.model_engines import (
35
+ engine_support_status as _engine_support_status,
36
+ )
37
+ from latticeai.services.model_engines import (
38
+ ensure_llamacpp_server as _ensure_llamacpp_server,
39
+ )
40
+ from latticeai.services.model_engines import (
41
+ ensure_lmstudio_server as _ensure_lmstudio_server,
42
+ )
43
+ from latticeai.services.model_engines import (
44
+ ensure_ollama_server as _ensure_ollama_server,
45
+ )
46
+ from latticeai.services.model_engines import (
47
+ ensure_vllm_server as _ensure_vllm_server,
48
+ )
49
+ from latticeai.services.model_engines import (
50
+ find_lmstudio_cli as _find_lmstudio_cli,
51
+ )
52
+ from latticeai.services.model_engines import (
53
+ get_ollama_pulled_models as _get_ollama_pulled_models,
54
+ )
55
+ from latticeai.services.model_engines import (
56
+ get_openai_compatible_server_models as _get_openai_compatible_server_models,
57
+ )
58
+ from latticeai.services.model_engines import (
59
+ local_binary as _local_binary,
60
+ )
61
+ from latticeai.services.model_engines import (
62
+ pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
63
+ )
64
+ from latticeai.services.model_engines import (
65
+ vllm_executable as _vllm_executable,
66
+ )
67
+ from latticeai.services.model_engines import (
68
+ vllm_metal_python as _vllm_metal_python,
69
+ )
70
+ from latticeai.services.model_engines import (
71
+ wait_for_openai_compatible_server as _wait_for_openai_compatible_server,
72
+ )
73
+ from latticeai.services.model_engines import (
74
+ windows_binary_candidates as _windows_binary_candidates,
75
+ )
76
+ from latticeai.services.model_errors import ModelRuntimeError
77
+
78
+
79
+ def _update_env_file(env_file: Path, key: str, value: str) -> None:
80
+ lines = []
81
+ found = False
82
+ if env_file.exists():
83
+ for line in env_file.read_text(encoding="utf-8").splitlines():
84
+ if line.startswith(f"{key}="):
85
+ lines.append(f"{key}={value}")
86
+ found = True
87
+ else:
88
+ lines.append(line)
89
+ if not found:
90
+ lines.append(f"{key}={value}")
91
+ env_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
92
+
93
+
94
+ LOCAL_SERVER_PROCESSES = _LOCAL_SERVER_PROCESSES
95
+ VLLM_METAL_ENV = Path.home() / ".venv-vllm-metal"
96
+ VLLM_METAL_BIN = VLLM_METAL_ENV / "bin" / "vllm"
97
+ VLLM_METAL_PYTHON = VLLM_METAL_ENV / "bin" / "python"
98
+ LMSTUDIO_BUNDLED_CLI = Path("/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms")
99
+
100
+ def windows_binary_candidates(binary: str) -> List[Path]:
101
+ return _windows_binary_candidates(binary)
102
+
103
+
104
+ def local_binary(binary: str) -> Optional[str]:
105
+ return _local_binary(binary)
106
+
107
+
108
+ def find_lmstudio_cli() -> Optional[str]:
109
+ return _find_lmstudio_cli()
110
+
111
+
112
+ def vllm_executable() -> Optional[str]:
113
+ return _vllm_executable()
114
+
115
+
116
+ def vllm_metal_python() -> Optional[str]:
117
+ return _vllm_metal_python()
118
+
119
+
120
+ def _json_request(
121
+ url: str,
122
+ *,
123
+ method: str = "GET",
124
+ payload: Optional[Dict[str, Any]] = None,
125
+ headers: Optional[Dict[str, str]] = None,
126
+ timeout: float = 10.0,
127
+ ) -> Dict[str, Any]:
128
+ data = None
129
+ req_headers = dict(headers or {})
130
+ if payload is not None:
131
+ data = json.dumps(payload).encode("utf-8")
132
+ req_headers.setdefault("Content-Type", "application/json")
133
+ req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
134
+ with urllib.request.urlopen(req, timeout=timeout) as res:
135
+ raw = res.read().decode("utf-8", errors="replace")
136
+ if not raw.strip():
137
+ return {}
138
+ return json.loads(raw)
139
+
140
+
141
+ def lmstudio_api_base() -> str:
142
+ return (os.getenv("LMSTUDIO_BASE_URL") or OPENAI_COMPATIBLE_PROVIDERS["lmstudio"]["base_url"]).rstrip("/")
143
+
144
+
145
+ def lmstudio_native_api_base() -> str:
146
+ base = lmstudio_api_base()
147
+ return base[:-3] if base.endswith("/v1") else base
148
+
149
+
150
+ def ensure_lmstudio_server() -> None:
151
+ return _ensure_lmstudio_server()
152
+
153
+
154
+ _LMSTUDIO_MODELS_CACHE: List[Dict[str, Any]] = []
155
+ _LMSTUDIO_MODELS_CACHE_TS: float = 0.0
156
+ _LMSTUDIO_MODELS_CACHE_TTL: float = 10.0
157
+
158
+
159
+ def get_lmstudio_models(*, force: bool = False) -> List[Dict[str, Any]]:
160
+ global _LMSTUDIO_MODELS_CACHE, _LMSTUDIO_MODELS_CACHE_TS
161
+ if not force and time.monotonic() - _LMSTUDIO_MODELS_CACHE_TS < _LMSTUDIO_MODELS_CACHE_TTL:
162
+ return _LMSTUDIO_MODELS_CACHE
163
+ try:
164
+ payload = _json_request(
165
+ f"{lmstudio_native_api_base()}/api/v1/models",
166
+ headers={"Authorization": f"Bearer {os.getenv('LMSTUDIO_API_KEY') or 'lmstudio'}"},
167
+ timeout=2.5,
168
+ )
169
+ except Exception:
170
+ return _LMSTUDIO_MODELS_CACHE
171
+ models = payload.get("models")
172
+ _LMSTUDIO_MODELS_CACHE = models if isinstance(models, list) else []
173
+ _LMSTUDIO_MODELS_CACHE_TS = time.monotonic()
174
+ return _LMSTUDIO_MODELS_CACHE
175
+
176
+
177
+ def _lmstudio_candidate_keys(model_name: str) -> List[str]:
178
+ raw = model_name.strip()
179
+ if not raw:
180
+ return []
181
+ slug = raw.split("/")[-1].lower()
182
+ slug = slug.replace("-gguf", "").replace("-awq", "")
183
+ parts = [p for p in slug.split("-") if p]
184
+ candidates = [raw.lower(), slug]
185
+ if parts:
186
+ candidates.append("-".join(parts[: min(4, len(parts))]))
187
+ return list(dict.fromkeys(candidates))
188
+
189
+
190
+ def _find_lmstudio_model_key(model_name: str, models: List[Dict[str, Any]]) -> Optional[str]:
191
+ if not models:
192
+ return None
193
+ candidate_keys = _lmstudio_candidate_keys(model_name)
194
+ exact = []
195
+ fuzzy = []
196
+ for item in models:
197
+ key = str(item.get("key") or "").strip()
198
+ display_name = str(item.get("display_name") or "").strip()
199
+ haystacks = [key.lower(), display_name.lower()]
200
+ if any(raw == key.lower() for raw in candidate_keys):
201
+ exact.append(key)
202
+ continue
203
+ if any(token and token in hay for token in candidate_keys for hay in haystacks):
204
+ fuzzy.append(key)
205
+ return next(iter(exact or fuzzy), None)
206
+
207
+
208
+ def ensure_lmstudio_model(model_name: str) -> Dict[str, Any]:
209
+ ensure_lmstudio_server()
210
+ auth_header = {"Authorization": f"Bearer {os.getenv('LMSTUDIO_API_KEY') or 'lmstudio'}"}
211
+ models = get_lmstudio_models()
212
+ found_key = _find_lmstudio_model_key(model_name, models)
213
+ model_key = found_key or model_name
214
+
215
+ if not found_key:
216
+ try:
217
+ job = _json_request(
218
+ f"{lmstudio_native_api_base()}/api/v1/models/download",
219
+ method="POST",
220
+ payload={"model": model_name},
221
+ headers=auth_header,
222
+ timeout=30,
223
+ )
224
+ except urllib.error.HTTPError as e:
225
+ detail = e.read().decode("utf-8", errors="replace")[-2000:]
226
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {detail or e.reason}")
227
+ except Exception as e:
228
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {e}")
229
+
230
+ status = str(job.get("status") or "")
231
+ job_id = str(job.get("job_id") or "")
232
+ if status not in {"completed", "already_downloaded"} and job_id:
233
+ deadline = time.time() + 3600
234
+ while time.time() < deadline:
235
+ polled = _json_request(
236
+ f"{lmstudio_native_api_base()}/api/v1/models/download/status/{job_id}",
237
+ headers=auth_header,
238
+ timeout=30,
239
+ )
240
+ polled_status = str(polled.get("status") or "")
241
+ if polled_status == "completed":
242
+ break
243
+ if polled_status == "failed":
244
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {polled}")
245
+ time.sleep(2)
246
+ else:
247
+ raise ModelRuntimeError(status_code=408, detail="LM Studio 모델 다운로드 시간이 초과되었습니다.")
248
+
249
+ models = get_lmstudio_models(force=True)
250
+ model_key = _find_lmstudio_model_key(model_name, models) or model_name
251
+
252
+ target = next((item for item in models if isinstance(item, dict) and item.get("key") == model_key), None)
253
+ loaded_instances = target.get("loaded_instances") if isinstance(target, dict) else None
254
+ if loaded_instances:
255
+ return {"provider": "lmstudio", "model": model_name, "resolved_model": model_key, "server_ready": True, "cached": True}
256
+
257
+ try:
258
+ loaded = _json_request(
259
+ f"{lmstudio_native_api_base()}/api/v1/models/load",
260
+ method="POST",
261
+ payload={"model": model_key, "context_length": 4096},
262
+ headers=auth_header,
263
+ timeout=120,
264
+ )
265
+ except urllib.error.HTTPError as e:
266
+ detail = e.read().decode("utf-8", errors="replace")[-2000:]
267
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {detail or e.reason}")
268
+ except Exception as e:
269
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {e}")
270
+
271
+ if str(loaded.get("status") or "") != "loaded":
272
+ raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {loaded}")
273
+
274
+ return {
275
+ "provider": "lmstudio",
276
+ "model": model_name,
277
+ "resolved_model": model_key,
278
+ "instance_id": loaded.get("instance_id"),
279
+ "server_ready": True,
280
+ "cached": False,
281
+ }
282
+
283
+ def engine_support_status(engine: str) -> Dict[str, Any]:
284
+ return _engine_support_status(engine)
285
+
286
+ def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str, Any]:
287
+ return _pull_ollama_model_with_progress(model_name, progress_emit)
288
+
289
+
290
+ def get_ollama_pulled_models() -> set:
291
+ return _get_ollama_pulled_models()
292
+
293
+
294
+ def get_openai_compatible_server_models(provider: str) -> List[str]:
295
+ return _get_openai_compatible_server_models(provider)
296
+
297
+
298
+ def ensure_ollama_server() -> None:
299
+ return _ensure_ollama_server()
300
+
301
+
302
+ def wait_for_openai_compatible_server(provider: str, model_name: Optional[str] = None, timeout: int = 45) -> bool:
303
+ return _wait_for_openai_compatible_server(provider, model_name=model_name, timeout=timeout)
304
+
305
+
306
+ def ensure_vllm_server(model_name: str) -> None:
307
+ return _ensure_vllm_server(model_name)
308
+
309
+
310
+ def ensure_llamacpp_server(model_name: str) -> None:
311
+ return _ensure_llamacpp_server(model_name)
312
+
313
+
314
+ def _safe_engine_install_plan(
315
+ engine: str,
316
+ *,
317
+ base_dir: Path,
318
+ ) -> Optional[Dict[str, Any]]:
319
+ try:
320
+ return _engine_install_plan(engine, base_dir=base_dir)
321
+ except Exception:
322
+ return None
323
+
324
+
325
+ def engine_installed(engine: str) -> bool:
326
+ if engine == "local_mlx":
327
+ return bool(
328
+ importlib.util.find_spec("mlx")
329
+ and (importlib.util.find_spec("mlx_vlm") or importlib.util.find_spec("mlx_lm"))
330
+ )
331
+ if engine == "ollama":
332
+ return local_binary("ollama") is not None
333
+ if engine == "vllm":
334
+ return vllm_metal_python() is not None or vllm_executable() is not None or importlib.util.find_spec("vllm") is not None
335
+ if engine == "lmstudio":
336
+ return find_lmstudio_cli() is not None or Path("/Applications/LM Studio.app").exists()
337
+ if engine == "llamacpp":
338
+ return shutil.which("llama-server") is not None
339
+ if engine in {"openai", "openrouter", "groq", "together", "xai"}:
340
+ return AsyncOpenAI is not None
341
+ return False
@@ -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))