ltcai 8.0.0 → 8.2.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 (183) hide show
  1. package/README.md +33 -72
  2. package/docs/CHANGELOG.md +52 -874
  3. package/docs/DEVELOPMENT.md +26 -3
  4. package/docs/LEGACY_COMPATIBILITY.md +3 -1
  5. package/docs/TRUST_MODEL.md +26 -26
  6. package/docs/WHY_LATTICE.md +28 -25
  7. package/docs/kg-schema.md +6 -4
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/embeddings.py +9 -1
  10. package/lattice_brain/graph/schema.py +6 -0
  11. package/lattice_brain/runtime/multi_agent.py +1 -1
  12. package/latticeai/__init__.py +1 -1
  13. package/latticeai/api/computer_use.py +2 -0
  14. package/latticeai/api/memory.py +13 -0
  15. package/latticeai/app_factory.py +43 -162
  16. package/latticeai/brain/__init__.py +1 -1
  17. package/latticeai/core/config.py +10 -0
  18. package/latticeai/core/local_embeddings.py +8 -0
  19. package/latticeai/core/marketplace.py +1 -1
  20. package/latticeai/core/mcp_registry.py +140 -1
  21. package/latticeai/core/tool_registry.py +10 -0
  22. package/latticeai/core/workspace_graph_trace.py +132 -0
  23. package/latticeai/core/workspace_memory.py +75 -0
  24. package/latticeai/core/workspace_os.py +135 -1283
  25. package/latticeai/core/workspace_os_utils.py +132 -0
  26. package/latticeai/core/workspace_permissions.py +99 -0
  27. package/latticeai/core/workspace_plugins.py +97 -0
  28. package/latticeai/core/workspace_runs.py +612 -0
  29. package/latticeai/core/workspace_skills.py +114 -0
  30. package/latticeai/core/workspace_snapshots.py +195 -0
  31. package/latticeai/core/workspace_timeline.py +107 -0
  32. package/latticeai/runtime/audit_runtime.py +64 -0
  33. package/latticeai/runtime/config_runtime.py +4 -0
  34. package/latticeai/runtime/sso_runtime.py +52 -0
  35. package/latticeai/services/architecture_readiness.py +6 -5
  36. package/latticeai/services/memory_service.py +215 -7
  37. package/latticeai/services/model_capability_registry.py +68 -0
  38. package/latticeai/services/model_engines.py +603 -0
  39. package/latticeai/services/model_loading.py +482 -0
  40. package/latticeai/services/model_recommendation.py +5 -0
  41. package/latticeai/services/model_runtime.py +146 -837
  42. package/latticeai/services/product_readiness.py +19 -18
  43. package/package.json +2 -2
  44. package/scripts/build_frontend_assets.mjs +11 -2
  45. package/src-tauri/Cargo.lock +1 -1
  46. package/src-tauri/Cargo.toml +1 -1
  47. package/src-tauri/tauri.conf.json +1 -1
  48. package/static/app/asset-manifest.json +11 -102
  49. package/static/app/assets/Act-D9jIknFd.js +1 -0
  50. package/static/app/assets/Brain-CFOtWbPN.js +321 -0
  51. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
  52. package/static/app/assets/Library-C5Q2yWee.js +1 -0
  53. package/static/app/assets/System-BLbjdr1_.js +1 -0
  54. package/static/app/assets/core-CwxXejkd.js +1 -2
  55. package/static/app/assets/index-BqammyNu.js +16 -0
  56. package/static/app/assets/index-ty1iGgZu.css +2 -0
  57. package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
  58. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
  59. package/static/app/index.html +3 -10
  60. package/static/app/theme-boot.js +8 -0
  61. package/static/sw.js +1 -1
  62. package/tools/__init__.py +2 -1
  63. package/tools/computer.py +21 -0
  64. package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
  65. package/docs/HANDOVER_v3.6.0.md +0 -46
  66. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
  67. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
  68. package/docs/V2_ARCHITECTURE.md +0 -561
  69. package/docs/V3_2_AUDIT.md +0 -82
  70. package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
  71. package/docs/V3_FRONTEND.md +0 -140
  72. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
  73. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
  74. package/docs/V4_1_VALIDATION_REPORT.md +0 -47
  75. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
  76. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
  77. package/docs/V4_2_VALIDATION_REPORT.md +0 -89
  78. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
  79. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
  80. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
  81. package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
  82. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
  83. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
  84. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
  85. package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
  86. package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
  87. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
  88. package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
  89. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
  90. package/docs/V4_3_VALIDATION_REPORT.md +0 -58
  91. package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
  92. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
  93. package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
  94. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
  95. package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
  96. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
  97. package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
  98. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
  99. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
  100. package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
  101. package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
  102. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
  103. package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
  104. package/docs/V4_5_1_UX_REPORT.md +0 -45
  105. package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
  106. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
  107. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
  108. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
  109. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
  110. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
  111. package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
  112. package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
  113. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
  114. package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
  115. package/frontend/index.html +0 -24
  116. package/frontend/openapi.json +0 -15425
  117. package/frontend/src/App.tsx +0 -243
  118. package/frontend/src/api/client.ts +0 -580
  119. package/frontend/src/api/openapi.ts +0 -17892
  120. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  121. package/frontend/src/components/FeedbackState.tsx +0 -45
  122. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  123. package/frontend/src/components/LivingBrain.tsx +0 -220
  124. package/frontend/src/components/ProductFlow.tsx +0 -171
  125. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  126. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  127. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  128. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  129. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  130. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  131. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  132. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  133. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  134. package/frontend/src/components/primitives.tsx +0 -392
  135. package/frontend/src/components/ui/badge.tsx +0 -27
  136. package/frontend/src/components/ui/button.tsx +0 -37
  137. package/frontend/src/components/ui/card.tsx +0 -22
  138. package/frontend/src/components/ui/input.tsx +0 -16
  139. package/frontend/src/components/ui/textarea.tsx +0 -16
  140. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  141. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  142. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  143. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  144. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  145. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  146. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  147. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  148. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  149. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  150. package/frontend/src/features/brain/brainData.ts +0 -246
  151. package/frontend/src/features/brain/graphLayout.ts +0 -37
  152. package/frontend/src/features/brain/types.ts +0 -150
  153. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  154. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  155. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  156. package/frontend/src/i18n.ts +0 -1303
  157. package/frontend/src/lib/utils.ts +0 -33
  158. package/frontend/src/main.tsx +0 -23
  159. package/frontend/src/pages/Act.tsx +0 -458
  160. package/frontend/src/pages/Ask.tsx +0 -14
  161. package/frontend/src/pages/Brain.tsx +0 -914
  162. package/frontend/src/pages/Capture.tsx +0 -258
  163. package/frontend/src/pages/Library.tsx +0 -486
  164. package/frontend/src/pages/System.tsx +0 -621
  165. package/frontend/src/routes.ts +0 -92
  166. package/frontend/src/store/appStore.ts +0 -94
  167. package/frontend/src/styles.css +0 -6579
  168. package/static/app/assets/Act-DOvf59ru.js +0 -2
  169. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  170. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  171. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  172. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  173. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  174. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  175. package/static/app/assets/System-D6t9jo9V.js +0 -2
  176. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  177. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  178. package/static/app/assets/index-C7g26IF6.css +0 -2
  179. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  180. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  181. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  182. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  183. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -0,0 +1,482 @@
1
+ """Model loading, prepare and stream logic extracted from model_runtime for server decomp.
2
+
3
+ This moves the large prepare_and_load_model and stream functions out of the monolith.
4
+ Re-exports will be added in model_runtime for compat.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import queue
12
+ import subprocess
13
+ import threading
14
+ from pathlib import Path
15
+ from typing import AsyncIterator, Dict, Optional
16
+
17
+ from fastapi import HTTPException, Request
18
+
19
+ # Late imports to avoid circulars during extraction
20
+ def _get_model_runtime_deps():
21
+ from .model_runtime import (
22
+ _download_allowed,
23
+ _download_block,
24
+ _engine_install_block,
25
+ _friendly_model_runtime_error,
26
+ _ModelResolution,
27
+ _model_runtime_compatibility,
28
+ _smoke_test_loaded_model,
29
+ download_hf_model,
30
+ engine_installed,
31
+ ensure_engine_ready,
32
+ ensure_llamacpp_server,
33
+ ensure_lmstudio_model,
34
+ ensure_ollama_server,
35
+ ensure_vllm_server,
36
+ get_current_user,
37
+ get_lmstudio_models,
38
+ get_ollama_pulled_models,
39
+ get_user_api_key,
40
+ hf_model_dir,
41
+ hf_model_ready,
42
+ local_binary,
43
+ MODEL_ENGINE_ALIASES,
44
+ model_download_progress_payload,
45
+ normalize_local_model_request,
46
+ parse_model_ref,
47
+ pull_ollama_model_with_progress,
48
+ router,
49
+ )
50
+ return {
51
+ "_download_allowed": _download_allowed,
52
+ "_download_block": _download_block,
53
+ "_engine_install_block": _engine_install_block,
54
+ "_friendly_model_runtime_error": _friendly_model_runtime_error,
55
+ "_ModelResolution": _ModelResolution,
56
+ "_model_runtime_compatibility": _model_runtime_compatibility,
57
+ "_smoke_test_loaded_model": _smoke_test_loaded_model,
58
+ "download_hf_model": download_hf_model,
59
+ "engine_installed": engine_installed,
60
+ "ensure_engine_ready": ensure_engine_ready,
61
+ "ensure_llamacpp_server": ensure_llamacpp_server,
62
+ "ensure_lmstudio_model": ensure_lmstudio_model,
63
+ "ensure_ollama_server": ensure_ollama_server,
64
+ "ensure_vllm_server": ensure_vllm_server,
65
+ "get_current_user": get_current_user,
66
+ "get_lmstudio_models": get_lmstudio_models,
67
+ "get_ollama_pulled_models": get_ollama_pulled_models,
68
+ "get_user_api_key": get_user_api_key,
69
+ "hf_model_dir": hf_model_dir,
70
+ "hf_model_ready": hf_model_ready,
71
+ "local_binary": local_binary,
72
+ "MODEL_ENGINE_ALIASES": MODEL_ENGINE_ALIASES,
73
+ "model_download_progress_payload": model_download_progress_payload,
74
+ "normalize_local_model_request": normalize_local_model_request,
75
+ "parse_model_ref": parse_model_ref,
76
+ "pull_ollama_model_with_progress": pull_ollama_model_with_progress,
77
+ "router": router,
78
+ }
79
+
80
+
81
+ async def prepare_and_load_model(
82
+ model_id: str,
83
+ request: Request,
84
+ engine: Optional[str] = None,
85
+ user_email: Optional[str] = None,
86
+ adapter_path: Optional[str] = None,
87
+ draft_model_id: Optional[str] = None,
88
+ allow_download: bool = False,
89
+ ) -> Dict[str, object]:
90
+ deps = _get_model_runtime_deps()
91
+ model_id = deps["normalize_local_model_request"](model_id, engine)
92
+ if not model_id:
93
+ raise HTTPException(status_code=400, detail="모델 식별자가 비어 있습니다.")
94
+
95
+ resolution = deps["_ModelResolution"].from_request(
96
+ model_id,
97
+ engine=engine,
98
+ user_email=user_email or deps["get_current_user"](request),
99
+ engine_aliases=deps["MODEL_ENGINE_ALIASES"],
100
+ )
101
+
102
+ parsed_provider, parsed_model = deps["parse_model_ref"](model_id)
103
+ if parsed_provider == "mlx":
104
+ parsed_provider = "local_mlx"
105
+ compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
106
+ if compatibility.get("supported") is False:
107
+ raise HTTPException(status_code=400, detail=compatibility)
108
+
109
+ local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
110
+ install_result: Dict[str, object] = {}
111
+ download_result: Optional[Dict[str, object]] = None
112
+
113
+ if parsed_provider in local_engines:
114
+ if not deps["engine_installed"](parsed_provider) and not deps["_download_allowed"](allow_download):
115
+ deps["_engine_install_block"](parsed_provider)
116
+ install_result = deps["ensure_engine_ready"](parsed_provider)
117
+
118
+ if parsed_provider == "local_mlx":
119
+ explicit_path = Path(parsed_model).expanduser()
120
+ if not explicit_path.exists() and not deps["hf_model_ready"](parsed_model, "local_mlx"):
121
+ if not deps["_download_allowed"](allow_download):
122
+ deps["_download_block"](parsed_provider, parsed_model)
123
+ download_result = deps["download_hf_model"](parsed_model, "local_mlx")
124
+ elif parsed_provider == "ollama":
125
+ deps["ensure_ollama_server"]()
126
+ ollama = deps["local_binary"]("ollama")
127
+ if not ollama:
128
+ raise HTTPException(status_code=400, detail="Ollama가 설치되지 않았습니다.")
129
+ if parsed_model not in deps["get_ollama_pulled_models"]():
130
+ if not deps["_download_allowed"](allow_download):
131
+ deps["_download_block"](parsed_provider, parsed_model)
132
+ completed = subprocess.run(
133
+ [ollama, "pull", parsed_model],
134
+ capture_output=True,
135
+ text=True,
136
+ timeout=900,
137
+ check=False,
138
+ )
139
+ if completed.returncode != 0:
140
+ raise HTTPException(status_code=500, detail=completed.stderr[-2000:] or "Ollama 모델 다운로드 실패")
141
+ download_result = {"provider": "ollama", "model": parsed_model, "returncode": completed.returncode}
142
+ elif parsed_provider == "vllm":
143
+ if not deps["hf_model_ready"](parsed_model, "vllm") and not deps["_download_allowed"](allow_download):
144
+ deps["_download_block"](parsed_provider, parsed_model)
145
+ deps["ensure_vllm_server"](parsed_model)
146
+ download_result = {"provider": "vllm", "model": parsed_model, "server_ready": True}
147
+ elif parsed_provider == "llamacpp":
148
+ if not deps["hf_model_ready"](parsed_model, "llamacpp") and not deps["_download_allowed"](allow_download):
149
+ deps["_download_block"](parsed_provider, parsed_model)
150
+ deps["ensure_llamacpp_server"](parsed_model)
151
+ download_result = {"provider": "llamacpp", "model": parsed_model, "server_ready": True}
152
+ elif parsed_provider == "lmstudio":
153
+ downloaded = {
154
+ str(item.get("key") or "").strip()
155
+ for item in deps["get_lmstudio_models"]()
156
+ if isinstance(item, dict)
157
+ }
158
+ if parsed_model not in downloaded and not deps["_download_allowed"](allow_download):
159
+ deps["_download_block"](parsed_provider, parsed_model)
160
+ ensured = deps["ensure_lmstudio_model"](parsed_model)
161
+ resolved_model = str(
162
+ ensured.get("instance_id")
163
+ or ensured.get("resolved_model")
164
+ or parsed_model
165
+ ).strip()
166
+ parsed_model = resolved_model
167
+ model_id = f"lmstudio:{resolved_model}"
168
+ download_result = ensured
169
+
170
+ effective_email = (user_email or deps["get_current_user"](request) or "").strip()
171
+ user_api_key = deps["get_user_api_key"](effective_email, parsed_provider) if parsed_provider != "local_mlx" else None
172
+ msg = await deps["router"].load_model(
173
+ model_id,
174
+ adapter_path,
175
+ draft_model_id=draft_model_id,
176
+ api_key_override=user_api_key,
177
+ owner=effective_email or None,
178
+ )
179
+ resolution.update_after_load(actual_current=deps["router"].current_model_id)
180
+ smoke_result: Dict[str, object] = {}
181
+ ready_to_chat = True
182
+ compat_status = "ok"
183
+ try:
184
+ smoke_result = await deps["_smoke_test_loaded_model"](resolution, api_key_override=user_api_key)
185
+ ready_to_chat = bool(smoke_result.get("ok"))
186
+ compat_status = str(smoke_result.get("status") or ("ok" if ready_to_chat else "degraded"))
187
+ except Exception as exc:
188
+ logging.warning("smoke test failed for %s: %s", resolution.load_id, exc)
189
+ compat_status = "unknown"
190
+ return {
191
+ "status": "ok",
192
+ "message": msg,
193
+ "model": model_id,
194
+ "current": deps["router"].current_model_id,
195
+ "engine": parsed_provider,
196
+ "installed_now": bool(install_result.get("installed_now")),
197
+ "download": download_result,
198
+ "resolution": resolution.to_dict(),
199
+ "downloaded": bool(download_result and not (isinstance(download_result, dict) and download_result.get("cached"))),
200
+ "loaded": True,
201
+ "ready_to_chat": ready_to_chat,
202
+ "compatibility_status": compat_status,
203
+ "smoke_test": smoke_result,
204
+ }
205
+
206
+
207
+ def sse_event(event: str, data: Dict[str, object]) -> str:
208
+ return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
209
+
210
+
211
+ async def prepare_and_load_model_stream(
212
+ model_id: str,
213
+ request: Request,
214
+ engine: Optional[str] = None,
215
+ user_email: Optional[str] = None,
216
+ allow_download: bool = False,
217
+ ) -> AsyncIterator[str]:
218
+ deps = _get_model_runtime_deps()
219
+ model_id = deps["normalize_local_model_request"](model_id, engine)
220
+ if not model_id:
221
+ raise HTTPException(status_code=400, detail="모델 식별자가 비어 있습니다.")
222
+
223
+ parsed_provider, parsed_model = deps["parse_model_ref"](model_id)
224
+ if parsed_provider == "mlx":
225
+ parsed_provider = "local_mlx"
226
+ compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
227
+ if compatibility.get("supported") is False:
228
+ raise HTTPException(status_code=400, detail=compatibility)
229
+
230
+ work_queue: "queue.Queue[Dict[str, object]]" = queue.Queue()
231
+ work_result: Dict[str, object] = {}
232
+
233
+ def emit_progress(payload: Dict[str, object]) -> None:
234
+ work_queue.put({"kind": "progress", "data": payload})
235
+
236
+ def blocking_prepare() -> None:
237
+ try:
238
+ local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
239
+ install_result: Dict[str, object] = {}
240
+ download_result: Optional[Dict[str, object]] = None
241
+ prepared_model_id = model_id
242
+ prepared_model_name = parsed_model
243
+
244
+ if parsed_provider in local_engines:
245
+ emit_progress(deps["model_download_progress_payload"](
246
+ "engine",
247
+ "실행 엔진을 확인하는 중입니다.",
248
+ percent=2,
249
+ indeterminate=True,
250
+ ))
251
+ if not deps["engine_installed"](parsed_provider) and not deps["_download_allowed"](allow_download):
252
+ deps["_engine_install_block"](parsed_provider)
253
+ install_result = deps["ensure_engine_ready"](parsed_provider)
254
+ emit_progress(deps["model_download_progress_payload"](
255
+ "engine",
256
+ "실행 엔진 준비가 완료되었습니다.",
257
+ percent=10,
258
+ indeterminate=False,
259
+ ))
260
+
261
+ if parsed_provider == "local_mlx":
262
+ explicit_path = Path(parsed_model).expanduser()
263
+ if explicit_path.exists():
264
+ download_result = {"model": parsed_model, "path": str(explicit_path), "cached": True}
265
+ emit_progress(deps["model_download_progress_payload"](
266
+ "download",
267
+ "로컬 모델 경로를 확인했습니다.",
268
+ percent=100,
269
+ detail=str(explicit_path),
270
+ eta_seconds=0,
271
+ ))
272
+ elif not deps["hf_model_ready"](parsed_model, "local_mlx"):
273
+ if not deps["_download_allowed"](allow_download):
274
+ deps["_download_block"](parsed_provider, parsed_model)
275
+ download_result = deps["download_hf_model"](parsed_model, "local_mlx", progress_emit=emit_progress)
276
+ else:
277
+ download_result = {"model": parsed_model, "path": str(deps["hf_model_dir"](parsed_model)), "cached": True}
278
+ emit_progress(deps["model_download_progress_payload"](
279
+ "download",
280
+ "이미 다운로드된 모델을 확인했습니다.",
281
+ percent=100,
282
+ eta_seconds=0,
283
+ ))
284
+ elif parsed_provider == "ollama":
285
+ emit_progress(deps["model_download_progress_payload"](
286
+ "engine",
287
+ "Ollama 서버를 확인하는 중입니다.",
288
+ percent=12,
289
+ indeterminate=True,
290
+ ))
291
+ deps["ensure_ollama_server"]()
292
+ if parsed_model not in deps["get_ollama_pulled_models"]():
293
+ if not deps["_download_allowed"](allow_download):
294
+ deps["_download_block"](parsed_provider, parsed_model)
295
+ download_result = deps["pull_ollama_model_with_progress"](parsed_model, progress_emit=emit_progress)
296
+ else:
297
+ download_result = {"provider": "ollama", "model": parsed_model, "cached": True}
298
+ emit_progress(deps["model_download_progress_payload"](
299
+ "download",
300
+ "이미 다운로드된 Ollama 모델을 확인했습니다.",
301
+ percent=100,
302
+ detail=parsed_model,
303
+ eta_seconds=0,
304
+ ))
305
+ elif parsed_provider == "vllm":
306
+ if not deps["hf_model_ready"](parsed_model, "vllm"):
307
+ if not deps["_download_allowed"](allow_download):
308
+ deps["_download_block"](parsed_provider, parsed_model)
309
+ download_result = deps["download_hf_model"](parsed_model, "vllm", progress_emit=emit_progress)
310
+ else:
311
+ download_result = {"provider": "vllm", "model": parsed_model, "cached": True}
312
+ emit_progress(deps["model_download_progress_payload"](
313
+ "download",
314
+ "이미 다운로드된 모델을 확인했습니다.",
315
+ percent=100,
316
+ detail=parsed_model,
317
+ eta_seconds=0,
318
+ ))
319
+ emit_progress(deps["model_download_progress_payload"](
320
+ "server",
321
+ "vLLM 서버를 시작하는 중입니다.",
322
+ percent=92,
323
+ indeterminate=True,
324
+ ))
325
+ deps["ensure_vllm_server"](parsed_model)
326
+ download_result = {**(download_result or {}), "provider": "vllm", "model": parsed_model, "server_ready": True}
327
+ elif parsed_provider == "llamacpp":
328
+ if not deps["hf_model_ready"](parsed_model, "llamacpp"):
329
+ if not deps["_download_allowed"](allow_download):
330
+ deps["_download_block"](parsed_provider, parsed_model)
331
+ download_result = deps["download_hf_model"](parsed_model, "llamacpp", progress_emit=emit_progress)
332
+ else:
333
+ download_result = {"provider": "llamacpp", "model": parsed_model, "cached": True}
334
+ emit_progress(deps["model_download_progress_payload"](
335
+ "download",
336
+ "이미 다운로드된 GGUF 모델을 확인했습니다.",
337
+ percent=100,
338
+ detail=parsed_model,
339
+ eta_seconds=0,
340
+ ))
341
+ emit_progress(deps["model_download_progress_payload"](
342
+ "server",
343
+ "llama.cpp 서버를 시작하는 중입니다.",
344
+ percent=92,
345
+ indeterminate=True,
346
+ ))
347
+ deps["ensure_llamacpp_server"](parsed_model)
348
+ download_result = {**(download_result or {}), "provider": "llamacpp", "model": parsed_model, "server_ready": True}
349
+ elif parsed_provider == "lmstudio":
350
+ downloaded = {
351
+ str(item.get("key") or "").strip()
352
+ for item in deps["get_lmstudio_models"]()
353
+ if isinstance(item, dict)
354
+ }
355
+ if parsed_model not in downloaded and not deps["_download_allowed"](allow_download):
356
+ deps["_download_block"](parsed_provider, parsed_model)
357
+ emit_progress(deps["model_download_progress_payload"](
358
+ "download",
359
+ "LM Studio 모델을 확인하는 중입니다.",
360
+ percent=35,
361
+ indeterminate=True,
362
+ ))
363
+ ensured = deps["ensure_lmstudio_model"](parsed_model)
364
+ resolved_model = str(
365
+ ensured.get("instance_id")
366
+ or ensured.get("resolved_model")
367
+ or parsed_model
368
+ ).strip()
369
+ prepared_model_name = resolved_model
370
+ prepared_model_id = f"lmstudio:{resolved_model}"
371
+ download_result = ensured
372
+ else:
373
+ emit_progress(deps["model_download_progress_payload"](
374
+ "engine",
375
+ "모델 연결을 준비하는 중입니다.",
376
+ percent=30,
377
+ indeterminate=True,
378
+ ))
379
+
380
+ work_result.update({
381
+ "model_id": prepared_model_id,
382
+ "parsed_provider": parsed_provider,
383
+ "parsed_model": prepared_model_name,
384
+ "install_result": install_result,
385
+ "download_result": download_result,
386
+ })
387
+ work_queue.put({"kind": "done"})
388
+ except HTTPException as exc:
389
+ work_queue.put({"kind": "error", "status_code": exc.status_code, "detail": exc.detail})
390
+ except Exception as exc:
391
+ logging.exception("model prepare stream worker failed")
392
+ work_queue.put({
393
+ "kind": "error",
394
+ "status_code": 500,
395
+ "detail": deps["_friendly_model_runtime_error"](exc, model_id=model_id, engine=parsed_provider),
396
+ })
397
+
398
+ worker = threading.Thread(target=blocking_prepare, daemon=True)
399
+ worker.start()
400
+
401
+ while True:
402
+ item = await asyncio.to_thread(work_queue.get)
403
+ kind = item.get("kind")
404
+ if kind == "progress":
405
+ yield sse_event("progress", item["data"])
406
+ elif kind == "error":
407
+ raise HTTPException(
408
+ status_code=int(item.get("status_code") or 500),
409
+ detail=item.get("detail") or "모델 준비에 실패했습니다.",
410
+ )
411
+ elif kind == "done":
412
+ break
413
+
414
+ prepared_model_id = str(work_result.get("model_id") or model_id)
415
+ prepared_provider = str(work_result.get("parsed_provider") or parsed_provider)
416
+ install_result = work_result.get("install_result") or {}
417
+ download_result = work_result.get("download_result")
418
+
419
+ yield sse_event("progress", deps["model_download_progress_payload"](
420
+ "load",
421
+ "모델을 메모리에 로드하는 중입니다.",
422
+ percent=96,
423
+ indeterminate=True,
424
+ ))
425
+
426
+ effective_email = (user_email or deps["get_current_user"](request) or "").strip()
427
+ user_api_key = deps["get_user_api_key"](effective_email, prepared_provider) if prepared_provider != "local_mlx" else None
428
+ msg = await deps["router"].load_model(
429
+ prepared_model_id,
430
+ None,
431
+ draft_model_id=None,
432
+ api_key_override=user_api_key,
433
+ owner=effective_email or None,
434
+ )
435
+ resolution_stream = deps["_ModelResolution"].from_request(
436
+ prepared_model_id,
437
+ engine=prepared_provider,
438
+ user_email=effective_email or None,
439
+ engine_aliases=deps["MODEL_ENGINE_ALIASES"],
440
+ )
441
+ resolution_stream.update_after_load(actual_current=deps["router"].current_model_id)
442
+ yield sse_event("progress", deps["model_download_progress_payload"](
443
+ "smoke_test",
444
+ "채팅 호환성 테스트 중입니다.",
445
+ percent=98,
446
+ indeterminate=True,
447
+ ))
448
+ smoke_result: Dict[str, object] = {}
449
+ ready_to_chat = True
450
+ compat_status = "ok"
451
+ try:
452
+ smoke_result = await deps["_smoke_test_loaded_model"](resolution_stream, api_key_override=user_api_key)
453
+ ready_to_chat = bool(smoke_result.get("ok"))
454
+ compat_status = str(smoke_result.get("status") or ("ok" if ready_to_chat else "degraded"))
455
+ except Exception as exc:
456
+ logging.warning("smoke test (stream) failed for %s: %s", resolution_stream.load_id, exc)
457
+ compat_status = "unknown"
458
+ result = {
459
+ "status": "ok",
460
+ "message": msg,
461
+ "model": prepared_model_id,
462
+ "current": deps["router"].current_model_id,
463
+ "engine": prepared_provider,
464
+ "installed_now": bool(isinstance(install_result, dict) and install_result.get("installed_now")),
465
+ "download": download_result,
466
+ "resolution": resolution_stream.to_dict(),
467
+ "downloaded": bool(download_result and not (isinstance(download_result, dict) and download_result.get("cached"))),
468
+ "loaded": True,
469
+ "ready_to_chat": ready_to_chat,
470
+ "compatibility_status": compat_status,
471
+ "smoke_test": smoke_result,
472
+ }
473
+ yield sse_event("progress", deps["model_download_progress_payload"](
474
+ "done",
475
+ "모델 준비가 완료되었습니다.",
476
+ percent=100,
477
+ eta_seconds=0,
478
+ ))
479
+ yield sse_event("done", result)
480
+
481
+
482
+ # To maintain exact public API, model_runtime will re-export these.
@@ -34,7 +34,12 @@ _APPLE_ONLY_ENGINES = {"local_mlx"}
34
34
  _FAMILY_ORDER = [
35
35
  "Gemma 4",
36
36
  "Qwen3-VL",
37
+ "Qwen2.5-VL",
37
38
  "Llama 4",
39
+ "Llama 3.2 Vision",
40
+ "Phi Vision",
41
+ "Moondream",
42
+ "Pixtral",
38
43
  ]
39
44
 
40
45
  _SIZE_RE = re.compile(r"([\d.]+)\s*(TB|GB|MB)", re.IGNORECASE)