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.
Files changed (289) hide show
  1. package/README.md +47 -54
  2. package/docs/CHANGELOG.md +94 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
  6. package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -2
  10. package/docs/PERMISSION_MODE.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/architecture.md +6 -2
  14. package/docs/kg-schema.md +2 -2
  15. package/docs/v11.3.0_PLAN.md +202 -0
  16. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  17. package/lattice_brain/__init__.py +1 -1
  18. package/lattice_brain/gates.py +125 -0
  19. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  20. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  21. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  22. package/lattice_brain/graph/_kg_common/text.py +479 -0
  23. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  24. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  25. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  26. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  27. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  28. package/lattice_brain/graph/fusion.py +35 -4
  29. package/lattice_brain/graph/projection/__init__.py +42 -0
  30. package/lattice_brain/graph/projection/curation.py +500 -0
  31. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
  32. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  33. package/lattice_brain/graph/retrieval/context.py +197 -0
  34. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  35. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  36. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  37. package/lattice_brain/graph/retrieval/signals.py +95 -0
  38. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  39. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  40. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  41. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  42. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  43. package/lattice_brain/graph/schema.py +9 -0
  44. package/lattice_brain/graph/store.py +9 -0
  45. package/lattice_brain/graph/vector_index/selector.py +32 -2
  46. package/lattice_brain/ingestion/__init__.py +130 -0
  47. package/lattice_brain/ingestion/_contract.py +90 -0
  48. package/lattice_brain/ingestion/constants.py +127 -0
  49. package/lattice_brain/ingestion/folder_scan.py +57 -0
  50. package/lattice_brain/ingestion/folders.py +258 -0
  51. package/lattice_brain/ingestion/hashing.py +26 -0
  52. package/lattice_brain/ingestion/jobs_api.py +107 -0
  53. package/lattice_brain/ingestion/models.py +80 -0
  54. package/lattice_brain/ingestion/pipeline.py +486 -0
  55. package/lattice_brain/ingestion/quality.py +209 -0
  56. package/lattice_brain/ingestion/routing.py +295 -0
  57. package/lattice_brain/multimodal/__init__.py +164 -0
  58. package/lattice_brain/multimodal/audio.py +77 -0
  59. package/lattice_brain/multimodal/common.py +118 -0
  60. package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
  61. package/lattice_brain/multimodal/ports.py +169 -0
  62. package/lattice_brain/multimodal/video.py +410 -0
  63. package/lattice_brain/portability/__init__.py +90 -0
  64. package/lattice_brain/portability/_contract.py +42 -0
  65. package/lattice_brain/portability/backups.py +338 -0
  66. package/lattice_brain/portability/bundles.py +136 -0
  67. package/lattice_brain/portability/constants.py +93 -0
  68. package/lattice_brain/portability/fsops.py +138 -0
  69. package/lattice_brain/portability/service.py +41 -0
  70. package/lattice_brain/portability/sharing.py +714 -0
  71. package/lattice_brain/runtime/__init__.py +1 -1
  72. package/lattice_brain/runtime/multi_agent.py +1 -1
  73. package/lattice_brain/sealed_box.py +244 -0
  74. package/lattice_brain/synthesis.py +24 -1
  75. package/latticeai/__init__.py +1 -1
  76. package/latticeai/api/brain_intelligence.py +4 -0
  77. package/latticeai/api/chat.py +11 -0
  78. package/latticeai/api/chat_helpers.py +16 -3
  79. package/latticeai/api/chat_hybrid.py +32 -1
  80. package/latticeai/api/chronicle.py +63 -0
  81. package/latticeai/api/features.py +70 -0
  82. package/latticeai/api/local_files.py +102 -0
  83. package/latticeai/api/portability.py +39 -4
  84. package/latticeai/api/review_queue.py +126 -0
  85. package/latticeai/api/search.py +16 -2
  86. package/latticeai/core/agent/__init__.py +93 -0
  87. package/latticeai/core/agent/_contract.py +79 -0
  88. package/latticeai/core/agent/context.py +57 -0
  89. package/latticeai/core/agent/deps.py +125 -0
  90. package/latticeai/core/agent/execution.py +622 -0
  91. package/latticeai/core/agent/planning.py +145 -0
  92. package/latticeai/core/agent/recovery.py +157 -0
  93. package/latticeai/core/agent/runtime.py +210 -0
  94. package/latticeai/core/agent/verification.py +231 -0
  95. package/latticeai/core/config.py +4 -1
  96. package/latticeai/core/context_builder.py +6 -3
  97. package/latticeai/core/embedding_providers/__init__.py +151 -0
  98. package/latticeai/core/embedding_providers/base.py +199 -0
  99. package/latticeai/core/embedding_providers/captions.py +162 -0
  100. package/latticeai/core/embedding_providers/profiles.py +126 -0
  101. package/latticeai/core/embedding_providers/text.py +350 -0
  102. package/latticeai/core/embedding_providers/vision.py +352 -0
  103. package/latticeai/core/file_generation/__init__.py +115 -0
  104. package/latticeai/core/file_generation/bundles.py +76 -0
  105. package/latticeai/core/file_generation/extraction.py +154 -0
  106. package/latticeai/core/file_generation/inference.py +235 -0
  107. package/latticeai/core/file_generation/orchestration.py +152 -0
  108. package/latticeai/core/file_generation/prompting.py +117 -0
  109. package/latticeai/core/file_generation/repair.py +114 -0
  110. package/latticeai/core/file_generation/sanitize.py +61 -0
  111. package/latticeai/core/file_generation/validation.py +201 -0
  112. package/latticeai/core/legacy_compatibility.py +1 -1
  113. package/latticeai/core/marketplace.py +1 -1
  114. package/latticeai/core/messages.py +152 -0
  115. package/latticeai/core/model_compat.py +73 -2
  116. package/latticeai/core/workspace_os_constants.py +1 -1
  117. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  118. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  119. package/latticeai/integrations/telegram_bot/config.py +86 -0
  120. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  121. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  122. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  123. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  124. package/latticeai/models/model_providers.py +12 -4
  125. package/latticeai/models/router/__init__.py +88 -0
  126. package/latticeai/models/router/_contract.py +66 -0
  127. package/latticeai/models/router/branding.py +56 -0
  128. package/latticeai/models/router/catalog.py +69 -0
  129. package/latticeai/models/router/documents.py +199 -0
  130. package/latticeai/models/router/errors.py +37 -0
  131. package/latticeai/models/router/generation.py +258 -0
  132. package/latticeai/models/router/loading.py +291 -0
  133. package/latticeai/models/router/local_models.py +85 -0
  134. package/latticeai/models/router/registry.py +147 -0
  135. package/latticeai/runtime/build_phases/__init__.py +82 -0
  136. package/latticeai/runtime/build_phases/features.py +407 -0
  137. package/latticeai/runtime/build_phases/foundation.py +555 -0
  138. package/latticeai/runtime/build_phases/web.py +492 -0
  139. package/latticeai/runtime/chat_wiring.py +4 -0
  140. package/latticeai/runtime/feature_toggle_wiring.py +163 -0
  141. package/latticeai/runtime/router_registration.py +11 -0
  142. package/latticeai/runtime/runtime_context.py +1 -0
  143. package/latticeai/services/app_context.py +8 -0
  144. package/latticeai/services/architecture_readiness.py +48 -19
  145. package/latticeai/services/automation_intelligence.py +22 -2
  146. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  147. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  148. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  149. package/latticeai/services/brain_intelligence/constants.py +47 -0
  150. package/latticeai/services/brain_intelligence/digest.py +258 -0
  151. package/latticeai/services/brain_intelligence/health.py +331 -0
  152. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  153. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  154. package/latticeai/services/brain_intelligence/service.py +48 -0
  155. package/latticeai/services/chronicle.py +557 -0
  156. package/latticeai/services/command_center.py +10 -4
  157. package/latticeai/services/feature_toggles.py +502 -0
  158. package/latticeai/services/folder_watch.py +122 -1
  159. package/latticeai/services/hybrid_chat.py +56 -5
  160. package/latticeai/services/interop_bridges.py +978 -0
  161. package/latticeai/services/memory_service/__init__.py +52 -0
  162. package/latticeai/services/memory_service/_contract.py +100 -0
  163. package/latticeai/services/memory_service/brief.py +431 -0
  164. package/latticeai/services/memory_service/constants.py +57 -0
  165. package/latticeai/services/memory_service/maintenance.py +138 -0
  166. package/latticeai/services/memory_service/manager.py +186 -0
  167. package/latticeai/services/memory_service/proof.py +136 -0
  168. package/latticeai/services/memory_service/recall.py +225 -0
  169. package/latticeai/services/memory_service/service.py +48 -0
  170. package/latticeai/services/memory_service/stores.py +110 -0
  171. package/latticeai/services/model_capability_registry.py +434 -261
  172. package/latticeai/services/model_catalog.py +95 -61
  173. package/latticeai/services/model_recommendation.py +18 -11
  174. package/latticeai/services/model_runtime/__init__.py +322 -0
  175. package/latticeai/services/model_runtime/cloud.py +87 -0
  176. package/latticeai/services/model_runtime/download.py +282 -0
  177. package/latticeai/services/model_runtime/engines.py +341 -0
  178. package/latticeai/services/model_runtime/loading.py +178 -0
  179. package/latticeai/services/model_runtime/service.py +129 -0
  180. package/latticeai/services/model_runtime/state.py +131 -0
  181. package/latticeai/services/model_runtime/status.py +255 -0
  182. package/latticeai/services/multimodal_ports.py +26 -1
  183. package/latticeai/services/obsidian_bridge.py +16 -25
  184. package/latticeai/services/product_readiness.py +15 -7
  185. package/latticeai/services/search_service.py +149 -2
  186. package/latticeai/services/tool_dispatch.py +4 -0
  187. package/latticeai/setup/auto_setup.py +27 -30
  188. package/latticeai/setup/wizard/__init__.py +126 -0
  189. package/latticeai/setup/wizard/catalog.py +172 -0
  190. package/latticeai/setup/wizard/detect.py +323 -0
  191. package/latticeai/setup/wizard/install.py +348 -0
  192. package/latticeai/setup/wizard/paths.py +168 -0
  193. package/latticeai/setup/wizard/plans.py +74 -0
  194. package/latticeai/setup/wizard/recommend.py +320 -0
  195. package/package.json +6 -2
  196. package/scripts/bump_version.py +14 -0
  197. package/scripts/capture_release_evidence.mjs +33 -21
  198. package/scripts/check_current_release_docs.mjs +1 -1
  199. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  200. package/scripts/check_max_file_lines.mjs +102 -0
  201. package/scripts/check_release_evidence_bound.mjs +30 -15
  202. package/scripts/check_screenshot_pixel_delta.py +34 -4
  203. package/scripts/check_server_i18n.mjs +2 -0
  204. package/scripts/generate_rust_parity_fixtures.py +562 -0
  205. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  206. package/scripts/release_screen_claims.json +44 -2
  207. package/scripts/verify_hf_model_registry.py +253 -218
  208. package/src-tauri/Cargo.lock +361 -3
  209. package/src-tauri/Cargo.toml +6 -1
  210. package/src-tauri/src/backend.rs +349 -0
  211. package/src-tauri/src/folder.rs +33 -0
  212. package/src-tauri/src/main.rs +97 -399
  213. package/src-tauri/tauri.conf.json +1 -1
  214. package/static/app/asset-manifest.json +41 -37
  215. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  216. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  217. package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
  218. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  219. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  220. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  221. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  222. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  223. package/static/app/assets/Library-DwO3yZST.js +1 -0
  224. package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
  225. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  226. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  227. package/static/app/assets/System-DW8F-2xL.js +1 -0
  228. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  229. package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
  230. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  231. package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
  232. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  233. package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
  234. package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
  235. package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
  236. package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
  237. package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
  238. package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
  239. package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
  240. package/static/app/assets/index-_u5iUHDr.js +10 -0
  241. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  242. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  243. package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
  244. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  245. package/static/app/assets/search-BybIWPNd.js +1 -0
  246. package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
  247. package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
  248. package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
  249. package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
  250. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  251. package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
  252. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  253. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  254. package/static/app/index.html +4 -4
  255. package/static/sw.js +1 -1
  256. package/lattice_brain/graph/_kg_common.py +0 -1331
  257. package/lattice_brain/graph/discovery_index.py +0 -1141
  258. package/lattice_brain/graph/retrieval.py +0 -1120
  259. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  260. package/lattice_brain/ingestion.py +0 -1377
  261. package/lattice_brain/portability.py +0 -1210
  262. package/latticeai/core/agent.py +0 -1412
  263. package/latticeai/core/embedding_providers.py +0 -1196
  264. package/latticeai/core/file_generation.py +0 -1047
  265. package/latticeai/integrations/telegram_bot.py +0 -1390
  266. package/latticeai/models/router.py +0 -1007
  267. package/latticeai/runtime/build_phases.py +0 -1422
  268. package/latticeai/services/brain_intelligence.py +0 -967
  269. package/latticeai/services/memory_service.py +0 -1177
  270. package/latticeai/services/model_runtime.py +0 -1281
  271. package/latticeai/setup/wizard.py +0 -1277
  272. package/static/app/assets/Act-D0HWqtn0.js +0 -1
  273. package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
  274. package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
  275. package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
  276. package/static/app/assets/Capture-CT8v1StE.js +0 -1
  277. package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
  278. package/static/app/assets/Library-DDoxFE5c.js +0 -1
  279. package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
  280. package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
  281. package/static/app/assets/System-BRllvYXd.js +0 -1
  282. package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
  283. package/static/app/assets/brain-uMb_5hnO.js +0 -1
  284. package/static/app/assets/index-0AvoEBzJ.js +0 -10
  285. package/static/app/assets/input-B_5ZJ9oy.js +0 -1
  286. package/static/app/assets/primitives-CVwew78r.js +0 -1
  287. package/static/app/assets/search-DkhnOKZt.js +0 -1
  288. package/static/app/assets/utils-DcDMoZIe.js +0 -4
  289. 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
+ )