ltcai 8.9.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +46 -0
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +355 -952
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +70 -29
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +5 -14
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +2 -21
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +268 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +7 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1304
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Engine server management, pull, install and support logic extracted from model_runtime monolith.
|
|
2
2
|
|
|
3
3
|
This module is the home for engine-specific server starting, model pulling and install flows.
|
|
4
|
-
model_runtime
|
|
5
|
-
|
|
4
|
+
``model_runtime`` exposes compatibility callables, while application configuration
|
|
5
|
+
is passed explicitly through the bound runtime service. Circular imports are
|
|
6
|
+
avoided by late imports inside functions.
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -21,8 +22,8 @@ import urllib.request
|
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any, Dict, List, Optional
|
|
23
24
|
|
|
24
|
-
from
|
|
25
|
-
|
|
25
|
+
from latticeai.services.model_errors import ModelRuntimeError
|
|
26
|
+
from latticeai.services.model_catalog import ENGINE_INSTALLERS
|
|
26
27
|
from latticeai.services.process_audit import (
|
|
27
28
|
CommandConfirmationError,
|
|
28
29
|
append_process_audit_event,
|
|
@@ -148,7 +149,7 @@ def ensure_lmstudio_server() -> None:
|
|
|
148
149
|
|
|
149
150
|
cli = find_lmstudio_cli()
|
|
150
151
|
if not cli:
|
|
151
|
-
raise
|
|
152
|
+
raise ModelRuntimeError(status_code=400, detail="LM Studio CLI를 찾지 못했습니다. LM Studio를 설치한 뒤 다시 시도하세요.")
|
|
152
153
|
|
|
153
154
|
try:
|
|
154
155
|
subprocess.Popen(
|
|
@@ -158,7 +159,7 @@ def ensure_lmstudio_server() -> None:
|
|
|
158
159
|
start_new_session=True,
|
|
159
160
|
)
|
|
160
161
|
except Exception as e:
|
|
161
|
-
raise
|
|
162
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 서버 시작 실패: {e}")
|
|
162
163
|
|
|
163
164
|
deadline = time.time() + 45
|
|
164
165
|
while time.time() < deadline:
|
|
@@ -167,13 +168,13 @@ def ensure_lmstudio_server() -> None:
|
|
|
167
168
|
return
|
|
168
169
|
except Exception:
|
|
169
170
|
time.sleep(1)
|
|
170
|
-
raise
|
|
171
|
+
raise ModelRuntimeError(status_code=500, detail="LM Studio Local Server를 자동으로 시작하지 못했습니다.")
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
def ensure_ollama_server() -> None:
|
|
174
175
|
ollama = local_binary("ollama")
|
|
175
176
|
if not ollama:
|
|
176
|
-
raise
|
|
177
|
+
raise ModelRuntimeError(status_code=400, detail="Ollama가 설치되지 않았습니다.")
|
|
177
178
|
try:
|
|
178
179
|
probe = subprocess.run([ollama, "list"], capture_output=True, text=True, timeout=3, check=False)
|
|
179
180
|
if probe.returncode == 0:
|
|
@@ -195,7 +196,7 @@ def ensure_ollama_server() -> None:
|
|
|
195
196
|
except Exception:
|
|
196
197
|
pass
|
|
197
198
|
time.sleep(0.5)
|
|
198
|
-
raise
|
|
199
|
+
raise ModelRuntimeError(status_code=500, detail="Ollama 서버를 자동으로 시작하지 못했습니다.")
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
def get_openai_compatible_server_models(provider: str) -> List[str]:
|
|
@@ -262,7 +263,7 @@ def ensure_vllm_server(model_name: str) -> None:
|
|
|
262
263
|
vllm_bin = vllm_executable()
|
|
263
264
|
vllm_metal_py = vllm_metal_python()
|
|
264
265
|
if not vllm_bin and not vllm_metal_py and importlib.util.find_spec("vllm") is None:
|
|
265
|
-
raise
|
|
266
|
+
raise ModelRuntimeError(status_code=400, detail="vLLM runtime이 설치되지 않았습니다.")
|
|
266
267
|
|
|
267
268
|
local_dir = hf_model_dir(model_name)
|
|
268
269
|
if not vllm_metal_py and not hf_model_ready(model_name, "vllm"):
|
|
@@ -276,7 +277,7 @@ def ensure_vllm_server(model_name: str) -> None:
|
|
|
276
277
|
except subprocess.TimeoutExpired:
|
|
277
278
|
running.kill()
|
|
278
279
|
elif served_models:
|
|
279
|
-
raise
|
|
280
|
+
raise ModelRuntimeError(status_code=409, detail="다른 vLLM 서버가 이미 실행 중입니다. 현재 서버를 종료한 뒤 다시 시도하세요.")
|
|
280
281
|
|
|
281
282
|
running = LOCAL_SERVER_PROCESSES.get("vllm")
|
|
282
283
|
if running and running.poll() is None:
|
|
@@ -296,7 +297,7 @@ def ensure_vllm_server(model_name: str) -> None:
|
|
|
296
297
|
start_new_session=True,
|
|
297
298
|
)
|
|
298
299
|
if not wait_for_openai_compatible_server("vllm", model_name, timeout=90):
|
|
299
|
-
raise
|
|
300
|
+
raise ModelRuntimeError(status_code=500, detail="vLLM 서버가 모델을 자동 로드하지 못했습니다.")
|
|
300
301
|
|
|
301
302
|
|
|
302
303
|
def ensure_llamacpp_server(model_name: str) -> None:
|
|
@@ -313,15 +314,15 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
313
314
|
except subprocess.TimeoutExpired:
|
|
314
315
|
running.kill()
|
|
315
316
|
elif served_models:
|
|
316
|
-
raise
|
|
317
|
+
raise ModelRuntimeError(status_code=409, detail="다른 llama.cpp 서버가 이미 실행 중입니다. 현재 서버를 종료한 뒤 다시 시도하세요.")
|
|
317
318
|
if not shutil.which("llama-server"):
|
|
318
|
-
raise
|
|
319
|
+
raise ModelRuntimeError(status_code=400, detail="llama.cpp가 설치되지 않았습니다.")
|
|
319
320
|
if not hf_model_ready(model_name, "llamacpp"):
|
|
320
321
|
download_hf_model(model_name, "llamacpp")
|
|
321
322
|
|
|
322
323
|
gguf_files = sorted(hf_model_dir(model_name).rglob("*.gguf"))
|
|
323
324
|
if not gguf_files:
|
|
324
|
-
raise
|
|
325
|
+
raise ModelRuntimeError(status_code=500, detail="다운로드된 GGUF 파일을 찾지 못했습니다.")
|
|
325
326
|
|
|
326
327
|
preferred = next((p for p in gguf_files if "q4_k_m" in p.name.lower()), None)
|
|
327
328
|
model_file = preferred or gguf_files[0]
|
|
@@ -342,13 +343,13 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
342
343
|
start_new_session=True,
|
|
343
344
|
)
|
|
344
345
|
if not wait_for_openai_compatible_server("llamacpp", model_name, timeout=45):
|
|
345
|
-
raise
|
|
346
|
+
raise ModelRuntimeError(status_code=500, detail="llama.cpp 서버가 모델을 자동 로드하지 못했습니다.")
|
|
346
347
|
|
|
347
348
|
|
|
348
349
|
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str, object]:
|
|
349
350
|
ollama = local_binary("ollama")
|
|
350
351
|
if not ollama:
|
|
351
|
-
raise
|
|
352
|
+
raise ModelRuntimeError(status_code=400, detail="Ollama가 설치되지 않았습니다.")
|
|
352
353
|
if progress_emit:
|
|
353
354
|
progress_emit(_progress_payload(
|
|
354
355
|
"download",
|
|
@@ -402,7 +403,7 @@ def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict
|
|
|
402
403
|
|
|
403
404
|
if returncode != 0:
|
|
404
405
|
tail = "\n".join(lines[-12:])
|
|
405
|
-
raise
|
|
406
|
+
raise ModelRuntimeError(status_code=500, detail=tail[-2000:] or "Ollama 모델 다운로드 실패")
|
|
406
407
|
|
|
407
408
|
if progress_emit:
|
|
408
409
|
progress_emit(_progress_payload(
|
|
@@ -447,15 +448,17 @@ def engine_support_status(engine: str) -> Dict[str, object]:
|
|
|
447
448
|
return {"supported": True, "reason": None}
|
|
448
449
|
|
|
449
450
|
|
|
450
|
-
def _engine_install_command(
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
def _engine_install_command(
|
|
452
|
+
engine: str,
|
|
453
|
+
*,
|
|
454
|
+
base_dir: Optional[Path] = None,
|
|
455
|
+
) -> tuple[list[str], str, bool]:
|
|
453
456
|
if engine not in ENGINE_INSTALLERS:
|
|
454
|
-
raise
|
|
457
|
+
raise ModelRuntimeError(status_code=400, detail="지원하지 않는 엔진입니다.")
|
|
455
458
|
installer = ENGINE_INSTALLERS[engine]
|
|
456
459
|
required_binary = installer.get("requires_binary")
|
|
457
460
|
if required_binary and shutil.which(required_binary) is None:
|
|
458
|
-
raise
|
|
461
|
+
raise ModelRuntimeError(status_code=400, detail=f"{required_binary}가 설치되어 있지 않아 자동 설치할 수 없습니다.")
|
|
459
462
|
command = list(installer["command"])
|
|
460
463
|
|
|
461
464
|
if engine == "vllm" and sys.platform == "darwin" and platform.machine() == "arm64":
|
|
@@ -469,11 +472,15 @@ def _engine_install_command(engine: str) -> tuple[list[str], str, bool]:
|
|
|
469
472
|
"~/.venv-vllm-metal/bin/pip install vllm-metal",
|
|
470
473
|
]
|
|
471
474
|
requires_admin = bool(command and command[0] in {"apt", "apt-get", "dnf", "pacman"})
|
|
472
|
-
return command, str(
|
|
475
|
+
return command, str(base_dir or Path.cwd()), requires_admin
|
|
473
476
|
|
|
474
477
|
|
|
475
|
-
def engine_install_plan(
|
|
476
|
-
|
|
478
|
+
def engine_install_plan(
|
|
479
|
+
engine: str,
|
|
480
|
+
*,
|
|
481
|
+
base_dir: Optional[Path] = None,
|
|
482
|
+
) -> Dict[str, Any]:
|
|
483
|
+
command, cwd, requires_admin = _engine_install_command(engine, base_dir=base_dir)
|
|
477
484
|
return command_plan(
|
|
478
485
|
command,
|
|
479
486
|
name=f"engine:{engine}",
|
|
@@ -484,16 +491,21 @@ def engine_install_plan(engine: str) -> Dict[str, Any]:
|
|
|
484
491
|
)
|
|
485
492
|
|
|
486
493
|
|
|
487
|
-
def install_engine(
|
|
494
|
+
def install_engine(
|
|
495
|
+
engine: str,
|
|
496
|
+
confirmation_token: Optional[str] = None,
|
|
497
|
+
*,
|
|
498
|
+
base_dir: Optional[Path] = None,
|
|
499
|
+
) -> Dict[str, Any]:
|
|
488
500
|
from latticeai.services.model_runtime import engine_installed
|
|
489
501
|
|
|
490
|
-
command, cwd, _requires_admin = _engine_install_command(engine)
|
|
491
|
-
plan = engine_install_plan(engine)
|
|
502
|
+
command, cwd, _requires_admin = _engine_install_command(engine, base_dir=base_dir)
|
|
503
|
+
plan = engine_install_plan(engine, base_dir=base_dir)
|
|
492
504
|
try:
|
|
493
505
|
require_command_confirmation(command, confirmation_token, cwd=cwd, purpose="engine_install")
|
|
494
506
|
except CommandConfirmationError as exc:
|
|
495
507
|
append_process_audit_event("engine_install", plan=plan, status="denied", error=str(exc))
|
|
496
|
-
raise
|
|
508
|
+
raise ModelRuntimeError(
|
|
497
509
|
status_code=403,
|
|
498
510
|
detail={
|
|
499
511
|
"status": "confirmation_required",
|
|
@@ -514,7 +526,7 @@ def install_engine(engine: str, confirmation_token: Optional[str] = None) -> Dic
|
|
|
514
526
|
completed = subprocess.run(command, **run_kwargs)
|
|
515
527
|
except subprocess.TimeoutExpired:
|
|
516
528
|
append_process_audit_event("engine_install", plan=plan, status="timeout")
|
|
517
|
-
raise
|
|
529
|
+
raise ModelRuntimeError(status_code=408, detail="엔진 설치 시간이 초과되었습니다.")
|
|
518
530
|
except Exception as exc:
|
|
519
531
|
append_process_audit_event("engine_install", plan=plan, status="error", error=str(exc))
|
|
520
532
|
raise
|
|
@@ -576,6 +588,7 @@ async def _smoke_test_loaded_model(
|
|
|
576
588
|
resolution: Any,
|
|
577
589
|
*,
|
|
578
590
|
api_key_override: Optional[str] = None,
|
|
591
|
+
model_router: Any = None,
|
|
579
592
|
) -> Dict[str, object]:
|
|
580
593
|
"""로드 직후 짧은 채팅 테스트를 돌려 ready_to_chat 여부를 판정한다.
|
|
581
594
|
|
|
@@ -585,7 +598,6 @@ async def _smoke_test_loaded_model(
|
|
|
585
598
|
# late imports to avoid circular and keep lattice_brain/latticeai clean
|
|
586
599
|
try:
|
|
587
600
|
from latticeai.services.model_runtime import (
|
|
588
|
-
router as _router,
|
|
589
601
|
_LOCAL_SMOKE_ENGINES,
|
|
590
602
|
_SMOKE_PROMPT,
|
|
591
603
|
)
|
|
@@ -599,6 +611,9 @@ async def _smoke_test_loaded_model(
|
|
|
599
611
|
except Exception as e:
|
|
600
612
|
return {"ok": False, "reason": f"smoke import failed: {e}", "skipped": True}
|
|
601
613
|
|
|
614
|
+
if model_router is None:
|
|
615
|
+
return {"ok": False, "reason": "model router is not configured", "skipped": True}
|
|
616
|
+
|
|
602
617
|
if (getattr(resolution, "engine", "") or "").lower() not in _LOCAL_SMOKE_ENGINES:
|
|
603
618
|
profile = _ensure_compat_profile(getattr(resolution, "load_id", ""), getattr(resolution, "engine", ""))
|
|
604
619
|
return {
|
|
@@ -610,7 +625,7 @@ async def _smoke_test_loaded_model(
|
|
|
610
625
|
}
|
|
611
626
|
try:
|
|
612
627
|
text = await asyncio.wait_for(
|
|
613
|
-
|
|
628
|
+
model_router.generate(
|
|
614
629
|
_SMOKE_PROMPT,
|
|
615
630
|
context=None,
|
|
616
631
|
max_tokens=128,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Transport-neutral model runtime failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelRuntimeError(RuntimeError):
|
|
9
|
+
"""A model operation failure with an API-safe status and detail payload."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, status_code: int, detail: Any):
|
|
12
|
+
self.status_code = int(status_code)
|
|
13
|
+
self.detail = detail
|
|
14
|
+
super().__init__(str(detail))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["ModelRuntimeError"]
|
|
@@ -11,13 +11,15 @@ import logging
|
|
|
11
11
|
import queue
|
|
12
12
|
import subprocess
|
|
13
13
|
import threading
|
|
14
|
+
from functools import partial
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import AsyncIterator, Dict, Optional
|
|
16
|
+
from typing import Any, AsyncIterator, Dict, Optional
|
|
16
17
|
|
|
17
|
-
from
|
|
18
|
+
from .model_errors import ModelRuntimeError
|
|
18
19
|
|
|
19
|
-
# Late imports to avoid circulars during extraction
|
|
20
|
-
|
|
20
|
+
# Late imports to avoid circulars during extraction. The state argument is
|
|
21
|
+
# mandatory so this module cannot fall back to ambient process configuration.
|
|
22
|
+
def _get_model_runtime_deps(runtime_state: Any):
|
|
21
23
|
from .model_runtime import (
|
|
22
24
|
_download_allowed,
|
|
23
25
|
_download_block,
|
|
@@ -33,10 +35,8 @@ def _get_model_runtime_deps():
|
|
|
33
35
|
ensure_lmstudio_model,
|
|
34
36
|
ensure_ollama_server,
|
|
35
37
|
ensure_vllm_server,
|
|
36
|
-
get_current_user,
|
|
37
38
|
get_lmstudio_models,
|
|
38
39
|
get_ollama_pulled_models,
|
|
39
|
-
get_user_api_key,
|
|
40
40
|
hf_model_dir,
|
|
41
41
|
hf_model_ready,
|
|
42
42
|
local_binary,
|
|
@@ -45,27 +45,26 @@ def _get_model_runtime_deps():
|
|
|
45
45
|
normalize_local_model_request,
|
|
46
46
|
parse_model_ref,
|
|
47
47
|
pull_ollama_model_with_progress,
|
|
48
|
-
router,
|
|
49
48
|
)
|
|
50
49
|
return {
|
|
51
|
-
"_download_allowed": _download_allowed,
|
|
50
|
+
"_download_allowed": partial(_download_allowed, state=runtime_state),
|
|
52
51
|
"_download_block": _download_block,
|
|
53
52
|
"_engine_install_block": _engine_install_block,
|
|
54
53
|
"_friendly_model_runtime_error": _friendly_model_runtime_error,
|
|
55
54
|
"_ModelResolution": _ModelResolution,
|
|
56
55
|
"_model_runtime_compatibility": _model_runtime_compatibility,
|
|
57
|
-
"_smoke_test_loaded_model": _smoke_test_loaded_model,
|
|
56
|
+
"_smoke_test_loaded_model": partial(_smoke_test_loaded_model, state=runtime_state),
|
|
58
57
|
"download_hf_model": download_hf_model,
|
|
59
58
|
"engine_installed": engine_installed,
|
|
60
|
-
"ensure_engine_ready": ensure_engine_ready,
|
|
59
|
+
"ensure_engine_ready": partial(ensure_engine_ready, state=runtime_state),
|
|
61
60
|
"ensure_llamacpp_server": ensure_llamacpp_server,
|
|
62
61
|
"ensure_lmstudio_model": ensure_lmstudio_model,
|
|
63
62
|
"ensure_ollama_server": ensure_ollama_server,
|
|
64
63
|
"ensure_vllm_server": ensure_vllm_server,
|
|
65
|
-
"get_current_user": get_current_user,
|
|
64
|
+
"get_current_user": runtime_state.get_current_user,
|
|
66
65
|
"get_lmstudio_models": get_lmstudio_models,
|
|
67
66
|
"get_ollama_pulled_models": get_ollama_pulled_models,
|
|
68
|
-
"get_user_api_key": get_user_api_key,
|
|
67
|
+
"get_user_api_key": runtime_state.get_user_api_key,
|
|
69
68
|
"hf_model_dir": hf_model_dir,
|
|
70
69
|
"hf_model_ready": hf_model_ready,
|
|
71
70
|
"local_binary": local_binary,
|
|
@@ -74,23 +73,25 @@ def _get_model_runtime_deps():
|
|
|
74
73
|
"normalize_local_model_request": normalize_local_model_request,
|
|
75
74
|
"parse_model_ref": parse_model_ref,
|
|
76
75
|
"pull_ollama_model_with_progress": pull_ollama_model_with_progress,
|
|
77
|
-
"router": router,
|
|
76
|
+
"router": runtime_state.router,
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
|
|
81
80
|
async def prepare_and_load_model(
|
|
82
81
|
model_id: str,
|
|
83
|
-
request:
|
|
82
|
+
request: Any,
|
|
84
83
|
engine: Optional[str] = None,
|
|
85
84
|
user_email: Optional[str] = None,
|
|
86
85
|
adapter_path: Optional[str] = None,
|
|
87
86
|
draft_model_id: Optional[str] = None,
|
|
88
87
|
allow_download: bool = False,
|
|
88
|
+
*,
|
|
89
|
+
runtime_state: Any,
|
|
89
90
|
) -> Dict[str, object]:
|
|
90
|
-
deps = _get_model_runtime_deps()
|
|
91
|
+
deps = _get_model_runtime_deps(runtime_state)
|
|
91
92
|
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
92
93
|
if not model_id:
|
|
93
|
-
raise
|
|
94
|
+
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
94
95
|
|
|
95
96
|
resolution = deps["_ModelResolution"].from_request(
|
|
96
97
|
model_id,
|
|
@@ -104,7 +105,7 @@ async def prepare_and_load_model(
|
|
|
104
105
|
parsed_provider = "local_mlx"
|
|
105
106
|
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
106
107
|
if compatibility.get("supported") is False:
|
|
107
|
-
raise
|
|
108
|
+
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
108
109
|
|
|
109
110
|
local_engines = {"local_mlx", "ollama", "vllm", "lmstudio", "llamacpp"}
|
|
110
111
|
install_result: Dict[str, object] = {}
|
|
@@ -125,7 +126,7 @@ async def prepare_and_load_model(
|
|
|
125
126
|
deps["ensure_ollama_server"]()
|
|
126
127
|
ollama = deps["local_binary"]("ollama")
|
|
127
128
|
if not ollama:
|
|
128
|
-
raise
|
|
129
|
+
raise ModelRuntimeError(status_code=400, detail="Ollama가 설치되지 않았습니다.")
|
|
129
130
|
if parsed_model not in deps["get_ollama_pulled_models"]():
|
|
130
131
|
if not deps["_download_allowed"](allow_download):
|
|
131
132
|
deps["_download_block"](parsed_provider, parsed_model)
|
|
@@ -137,7 +138,7 @@ async def prepare_and_load_model(
|
|
|
137
138
|
check=False,
|
|
138
139
|
)
|
|
139
140
|
if completed.returncode != 0:
|
|
140
|
-
raise
|
|
141
|
+
raise ModelRuntimeError(status_code=500, detail=completed.stderr[-2000:] or "Ollama 모델 다운로드 실패")
|
|
141
142
|
download_result = {"provider": "ollama", "model": parsed_model, "returncode": completed.returncode}
|
|
142
143
|
elif parsed_provider == "vllm":
|
|
143
144
|
if not deps["hf_model_ready"](parsed_model, "vllm") and not deps["_download_allowed"](allow_download):
|
|
@@ -210,22 +211,24 @@ def sse_event(event: str, data: Dict[str, object]) -> str:
|
|
|
210
211
|
|
|
211
212
|
async def prepare_and_load_model_stream(
|
|
212
213
|
model_id: str,
|
|
213
|
-
request:
|
|
214
|
+
request: Any,
|
|
214
215
|
engine: Optional[str] = None,
|
|
215
216
|
user_email: Optional[str] = None,
|
|
216
217
|
allow_download: bool = False,
|
|
218
|
+
*,
|
|
219
|
+
runtime_state: Any,
|
|
217
220
|
) -> AsyncIterator[str]:
|
|
218
|
-
deps = _get_model_runtime_deps()
|
|
221
|
+
deps = _get_model_runtime_deps(runtime_state)
|
|
219
222
|
model_id = deps["normalize_local_model_request"](model_id, engine)
|
|
220
223
|
if not model_id:
|
|
221
|
-
raise
|
|
224
|
+
raise ModelRuntimeError(status_code=400, detail="모델 식별자가 비어 있습니다.")
|
|
222
225
|
|
|
223
226
|
parsed_provider, parsed_model = deps["parse_model_ref"](model_id)
|
|
224
227
|
if parsed_provider == "mlx":
|
|
225
228
|
parsed_provider = "local_mlx"
|
|
226
229
|
compatibility = deps["_model_runtime_compatibility"](parsed_model, engine=parsed_provider)
|
|
227
230
|
if compatibility.get("supported") is False:
|
|
228
|
-
raise
|
|
231
|
+
raise ModelRuntimeError(status_code=400, detail=compatibility)
|
|
229
232
|
|
|
230
233
|
work_queue: "queue.Queue[Dict[str, object]]" = queue.Queue()
|
|
231
234
|
work_result: Dict[str, object] = {}
|
|
@@ -385,7 +388,7 @@ async def prepare_and_load_model_stream(
|
|
|
385
388
|
"download_result": download_result,
|
|
386
389
|
})
|
|
387
390
|
work_queue.put({"kind": "done"})
|
|
388
|
-
except
|
|
391
|
+
except ModelRuntimeError as exc:
|
|
389
392
|
work_queue.put({"kind": "error", "status_code": exc.status_code, "detail": exc.detail})
|
|
390
393
|
except Exception as exc:
|
|
391
394
|
logging.exception("model prepare stream worker failed")
|
|
@@ -404,7 +407,7 @@ async def prepare_and_load_model_stream(
|
|
|
404
407
|
if kind == "progress":
|
|
405
408
|
yield sse_event("progress", item["data"])
|
|
406
409
|
elif kind == "error":
|
|
407
|
-
raise
|
|
410
|
+
raise ModelRuntimeError(
|
|
408
411
|
status_code=int(item.get("status_code") or 500),
|
|
409
412
|
detail=item.get("detail") or "모델 준비에 실패했습니다.",
|
|
410
413
|
)
|