ltcai 11.1.0 → 11.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -54
- package/docs/CHANGELOG.md +94 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/portability/sharing.py +714 -0
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +152 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +2 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +44 -2
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1377
- package/lattice_brain/portability.py +0 -1210
- package/latticeai/core/agent.py +0 -1412
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1422
- package/latticeai/services/brain_intelligence.py +0 -967
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1277
- package/static/app/assets/Act-D0HWqtn0.js +0 -1
- package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
- package/static/app/assets/Capture-CT8v1StE.js +0 -1
- package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
- package/static/app/assets/Library-DDoxFE5c.js +0 -1
- package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
- package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
- package/static/app/assets/System-BRllvYXd.js +0 -1
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/index-0AvoEBzJ.js +0 -10
- package/static/app/assets/input-B_5ZJ9oy.js +0 -1
- package/static/app/assets/primitives-CVwew78r.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
- package/static/app/assets/utils-DcDMoZIe.js +0 -4
- package/static/app/assets/workspace-LtRRSKTf.js +0 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Hugging Face model presence, download, and download progress.
|
|
2
|
+
|
|
3
|
+
Answers one question — "are the weights actually on this disk?" — and, when
|
|
4
|
+
they are not and the caller has consent, fetches them while emitting a progress
|
|
5
|
+
payload per file. The readiness check is deliberately format-aware: a GGUF
|
|
6
|
+
repo, an MLX 4-bit repo and a vLLM bf16 repo each prove completeness
|
|
7
|
+
differently.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from latticeai.core.quiet import quiet
|
|
19
|
+
from latticeai.models.router import hf_cache_model_dir, hf_model_dir
|
|
20
|
+
from latticeai.services.model_errors import ModelRuntimeError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def hf_model_ready(repo_id: str, provider: str = "local_mlx") -> bool:
|
|
24
|
+
model_dir = hf_model_dir(repo_id)
|
|
25
|
+
if provider in {"local_mlx", "vllm"} and (not model_dir.exists() or not model_dir.is_dir()):
|
|
26
|
+
hf_cache_repo = Path.home() / ".cache" / "huggingface" / "hub" / f"models--{repo_id.replace('/', '--')}"
|
|
27
|
+
if hf_cache_repo.exists() and any(hf_cache_repo.glob("snapshots/*")):
|
|
28
|
+
if provider == "vllm":
|
|
29
|
+
return True
|
|
30
|
+
return hf_cache_model_dir(repo_id) is not None
|
|
31
|
+
return False
|
|
32
|
+
if not model_dir.exists() or not model_dir.is_dir():
|
|
33
|
+
return False
|
|
34
|
+
if provider == "llamacpp":
|
|
35
|
+
return any(model_dir.rglob("*.gguf"))
|
|
36
|
+
has_config = (model_dir / "config.json").exists()
|
|
37
|
+
has_weights = any(model_dir.glob("*.safetensors")) or any(model_dir.glob("*.bin"))
|
|
38
|
+
has_tokenizer = (
|
|
39
|
+
(model_dir / "tokenizer.json").exists()
|
|
40
|
+
or (model_dir / "tokenizer.model").exists()
|
|
41
|
+
or (model_dir / "tokenizer_config.json").exists()
|
|
42
|
+
)
|
|
43
|
+
return has_config and has_weights and has_tokenizer
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def model_download_progress_payload(
|
|
47
|
+
stage: str,
|
|
48
|
+
message: str,
|
|
49
|
+
*,
|
|
50
|
+
percent: Optional[float] = None,
|
|
51
|
+
detail: Optional[str] = None,
|
|
52
|
+
downloaded_bytes: Optional[int] = None,
|
|
53
|
+
total_bytes: Optional[int] = None,
|
|
54
|
+
eta_seconds: Optional[float] = None,
|
|
55
|
+
file: Optional[str] = None,
|
|
56
|
+
indeterminate: bool = False,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
payload: Dict[str, Any] = {
|
|
59
|
+
"stage": stage,
|
|
60
|
+
"message": message,
|
|
61
|
+
"indeterminate": indeterminate,
|
|
62
|
+
"ts": time.time(),
|
|
63
|
+
}
|
|
64
|
+
if percent is not None:
|
|
65
|
+
payload["percent"] = max(0, min(100, round(float(percent), 1)))
|
|
66
|
+
if detail:
|
|
67
|
+
payload["detail"] = detail
|
|
68
|
+
if downloaded_bytes is not None:
|
|
69
|
+
payload["downloaded_bytes"] = max(0, int(downloaded_bytes))
|
|
70
|
+
if total_bytes is not None:
|
|
71
|
+
payload["total_bytes"] = max(0, int(total_bytes))
|
|
72
|
+
if eta_seconds is not None:
|
|
73
|
+
payload["eta_seconds"] = max(0, round(float(eta_seconds)))
|
|
74
|
+
if file:
|
|
75
|
+
payload["file"] = file
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def estimate_eta_seconds(started_at: float, percent: Optional[float]) -> Optional[float]:
|
|
80
|
+
if percent is None or percent <= 0 or percent >= 100:
|
|
81
|
+
return None
|
|
82
|
+
elapsed = max(0.0, time.time() - started_at)
|
|
83
|
+
return elapsed * (100.0 - percent) / percent
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def hf_repo_files_with_sizes(repo_id: str) -> List[Dict[str, Any]]:
|
|
87
|
+
from huggingface_hub import HfApi
|
|
88
|
+
|
|
89
|
+
api = HfApi()
|
|
90
|
+
try:
|
|
91
|
+
info = api.model_info(repo_id, files_metadata=True)
|
|
92
|
+
files = []
|
|
93
|
+
for sibling in getattr(info, "siblings", []) or []:
|
|
94
|
+
name = str(getattr(sibling, "rfilename", "") or "").strip()
|
|
95
|
+
if not name or name.endswith("/"):
|
|
96
|
+
continue
|
|
97
|
+
files.append({"name": name, "size": int(getattr(sibling, "size", 0) or 0)})
|
|
98
|
+
if files:
|
|
99
|
+
return files
|
|
100
|
+
except TypeError:
|
|
101
|
+
quiet()
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logging.warning("huggingface model_info failed for %s: %s", repo_id, e)
|
|
104
|
+
|
|
105
|
+
return [{"name": str(name), "size": 0} for name in api.list_repo_files(repo_id) if str(name).strip()]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def download_hf_model(
|
|
109
|
+
repo_id: str,
|
|
110
|
+
provider: str = "local_mlx",
|
|
111
|
+
progress_emit=None,
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
if importlib.util.find_spec("huggingface_hub") is None:
|
|
114
|
+
raise ModelRuntimeError(status_code=400, detail="huggingface_hub가 없습니다. 먼저 MLX runtime 설치를 진행해 주세요.")
|
|
115
|
+
|
|
116
|
+
target_dir = hf_model_dir(repo_id)
|
|
117
|
+
if hf_model_ready(repo_id, provider):
|
|
118
|
+
cached_dir = hf_cache_model_dir(repo_id) if provider == "local_mlx" else None
|
|
119
|
+
resolved_dir = cached_dir or target_dir
|
|
120
|
+
if progress_emit:
|
|
121
|
+
progress_emit(model_download_progress_payload(
|
|
122
|
+
"download",
|
|
123
|
+
"이미 다운로드된 모델을 확인했습니다.",
|
|
124
|
+
percent=100,
|
|
125
|
+
downloaded_bytes=0,
|
|
126
|
+
total_bytes=0,
|
|
127
|
+
eta_seconds=0,
|
|
128
|
+
))
|
|
129
|
+
return {"model": repo_id, "path": str(resolved_dir), "cached": True}
|
|
130
|
+
|
|
131
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
try:
|
|
133
|
+
from huggingface_hub import hf_hub_download
|
|
134
|
+
|
|
135
|
+
started_at = time.time()
|
|
136
|
+
all_files = hf_repo_files_with_sizes(repo_id)
|
|
137
|
+
if provider == "llamacpp":
|
|
138
|
+
ggufs = sorted(
|
|
139
|
+
[item for item in all_files if str(item["name"]).lower().endswith(".gguf")],
|
|
140
|
+
key=lambda item: str(item["name"]),
|
|
141
|
+
)
|
|
142
|
+
if not ggufs:
|
|
143
|
+
raise RuntimeError("GGUF 파일을 찾지 못했습니다.")
|
|
144
|
+
preference = ("q4_k_m", "q4_0", "q4_k_s", "q3_k_m", "q2_k")
|
|
145
|
+
selected_files = [
|
|
146
|
+
next(
|
|
147
|
+
(item for pref in preference for item in ggufs if pref in str(item["name"]).lower()),
|
|
148
|
+
ggufs[0],
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
else:
|
|
152
|
+
selected_files = all_files
|
|
153
|
+
|
|
154
|
+
total_bytes = sum(int(item.get("size") or 0) for item in selected_files) or None
|
|
155
|
+
downloaded_bytes = 0
|
|
156
|
+
total_files = max(1, len(selected_files))
|
|
157
|
+
if progress_emit:
|
|
158
|
+
progress_emit(model_download_progress_payload(
|
|
159
|
+
"download",
|
|
160
|
+
"모델 파일 정보를 확인했습니다.",
|
|
161
|
+
percent=0,
|
|
162
|
+
downloaded_bytes=0,
|
|
163
|
+
total_bytes=total_bytes,
|
|
164
|
+
indeterminate=total_bytes is None,
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
for index, item in enumerate(selected_files, start=1):
|
|
168
|
+
filename = str(item["name"])
|
|
169
|
+
size = int(item.get("size") or 0)
|
|
170
|
+
tqdm_class = None
|
|
171
|
+
if progress_emit:
|
|
172
|
+
current_percent = (
|
|
173
|
+
(downloaded_bytes / total_bytes) * 100 if total_bytes else ((index - 1) / total_files) * 100
|
|
174
|
+
)
|
|
175
|
+
progress_emit(model_download_progress_payload(
|
|
176
|
+
"download",
|
|
177
|
+
"모델 다운로드 중입니다.",
|
|
178
|
+
percent=current_percent,
|
|
179
|
+
detail=filename,
|
|
180
|
+
downloaded_bytes=downloaded_bytes,
|
|
181
|
+
total_bytes=total_bytes,
|
|
182
|
+
eta_seconds=estimate_eta_seconds(started_at, current_percent),
|
|
183
|
+
file=filename,
|
|
184
|
+
indeterminate=total_bytes is None and total_files <= 1,
|
|
185
|
+
))
|
|
186
|
+
try:
|
|
187
|
+
from tqdm.auto import tqdm as base_tqdm
|
|
188
|
+
|
|
189
|
+
downloaded_before = downloaded_bytes
|
|
190
|
+
last_emit = {"at": 0.0, "percent": -1.0}
|
|
191
|
+
|
|
192
|
+
def emit_byte_progress(
|
|
193
|
+
done_bytes: float,
|
|
194
|
+
# Bound per iteration: this callback outlives the loop
|
|
195
|
+
# body when a download runs long, and late binding
|
|
196
|
+
# would report every file's progress against the last
|
|
197
|
+
# file's offsets.
|
|
198
|
+
downloaded_before: int = downloaded_before,
|
|
199
|
+
size: Any = size,
|
|
200
|
+
index: int = index,
|
|
201
|
+
last_emit: dict = last_emit,
|
|
202
|
+
filename: str = filename,
|
|
203
|
+
) -> None:
|
|
204
|
+
done = max(0, int(done_bytes or 0))
|
|
205
|
+
if total_bytes:
|
|
206
|
+
aggregate = min(total_bytes, downloaded_before + done)
|
|
207
|
+
percent = (aggregate / total_bytes) * 100
|
|
208
|
+
else:
|
|
209
|
+
file_total = size or done
|
|
210
|
+
file_ratio = min(1.0, done / file_total) if file_total else 0.0
|
|
211
|
+
aggregate = downloaded_before + done
|
|
212
|
+
percent = ((index - 1) + file_ratio) / total_files * 100
|
|
213
|
+
now = time.time()
|
|
214
|
+
if percent < 100 and now - last_emit["at"] < 0.5 and percent - last_emit["percent"] < 0.3:
|
|
215
|
+
return
|
|
216
|
+
last_emit["at"] = now
|
|
217
|
+
last_emit["percent"] = percent
|
|
218
|
+
progress_emit(model_download_progress_payload(
|
|
219
|
+
"download",
|
|
220
|
+
"모델 다운로드 중입니다.",
|
|
221
|
+
percent=percent,
|
|
222
|
+
detail=filename,
|
|
223
|
+
downloaded_bytes=aggregate,
|
|
224
|
+
total_bytes=total_bytes,
|
|
225
|
+
eta_seconds=estimate_eta_seconds(started_at, percent),
|
|
226
|
+
file=filename,
|
|
227
|
+
indeterminate=total_bytes is None and total_files <= 1,
|
|
228
|
+
))
|
|
229
|
+
|
|
230
|
+
class ProgressTqdm(base_tqdm):
|
|
231
|
+
def update(self, n=1):
|
|
232
|
+
result = super().update(n)
|
|
233
|
+
emit_byte_progress(float(getattr(self, "n", 0) or 0))
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
tqdm_class = ProgressTqdm
|
|
237
|
+
except Exception:
|
|
238
|
+
tqdm_class = None
|
|
239
|
+
local_path = hf_hub_download(
|
|
240
|
+
repo_id=repo_id,
|
|
241
|
+
filename=filename,
|
|
242
|
+
local_dir=str(target_dir),
|
|
243
|
+
tqdm_class=tqdm_class,
|
|
244
|
+
)
|
|
245
|
+
if size <= 0:
|
|
246
|
+
try:
|
|
247
|
+
size = Path(local_path).stat().st_size
|
|
248
|
+
except OSError:
|
|
249
|
+
size = 0
|
|
250
|
+
downloaded_bytes += size
|
|
251
|
+
if progress_emit:
|
|
252
|
+
current_percent = (
|
|
253
|
+
(downloaded_bytes / total_bytes) * 100 if total_bytes else (index / total_files) * 100
|
|
254
|
+
)
|
|
255
|
+
progress_emit(model_download_progress_payload(
|
|
256
|
+
"download",
|
|
257
|
+
"모델 다운로드 중입니다.",
|
|
258
|
+
percent=current_percent,
|
|
259
|
+
detail=filename,
|
|
260
|
+
downloaded_bytes=downloaded_bytes,
|
|
261
|
+
total_bytes=total_bytes,
|
|
262
|
+
eta_seconds=estimate_eta_seconds(started_at, current_percent),
|
|
263
|
+
file=filename,
|
|
264
|
+
indeterminate=False,
|
|
265
|
+
))
|
|
266
|
+
|
|
267
|
+
if progress_emit:
|
|
268
|
+
progress_emit(model_download_progress_payload(
|
|
269
|
+
"download",
|
|
270
|
+
"모델 다운로드가 완료되었습니다.",
|
|
271
|
+
percent=100,
|
|
272
|
+
downloaded_bytes=downloaded_bytes,
|
|
273
|
+
total_bytes=total_bytes or downloaded_bytes,
|
|
274
|
+
eta_seconds=0,
|
|
275
|
+
))
|
|
276
|
+
except Exception as e:
|
|
277
|
+
raise ModelRuntimeError(status_code=500, detail=f"{repo_id} 다운로드 실패: {str(e)[-2000:]}")
|
|
278
|
+
|
|
279
|
+
if not hf_model_ready(repo_id, provider):
|
|
280
|
+
raise ModelRuntimeError(status_code=500, detail=f"{repo_id} 다운로드가 완료되지 않았습니다. 모델 파일을 찾지 못했습니다.")
|
|
281
|
+
|
|
282
|
+
return {"model": repo_id, "path": str(target_dir), "cached": False}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Local engine discovery, server hand-off, and the LM Studio HTTP client.
|
|
2
|
+
|
|
3
|
+
Thin wrappers over ``latticeai.services.model_engines`` that keep the historical
|
|
4
|
+
``model_runtime`` import path alive, plus the one piece of real logic that never
|
|
5
|
+
belonged in the engine layer: the LM Studio native API (list / download / load)
|
|
6
|
+
and its short-lived model cache.
|
|
7
|
+
|
|
8
|
+
``_LMSTUDIO_MODELS_CACHE`` and ``_LMSTUDIO_MODELS_CACHE_TS`` are rebindable
|
|
9
|
+
module state and therefore live here and nowhere else — the package
|
|
10
|
+
``__init__`` deliberately does not re-export them, because a
|
|
11
|
+
``from … import`` copy would freeze at import time and quietly disagree with
|
|
12
|
+
the live value.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import time
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS, AsyncOpenAI
|
|
28
|
+
from latticeai.services.model_engines import (
|
|
29
|
+
LOCAL_SERVER_PROCESSES as _LOCAL_SERVER_PROCESSES,
|
|
30
|
+
)
|
|
31
|
+
from latticeai.services.model_engines import (
|
|
32
|
+
engine_install_plan as _engine_install_plan,
|
|
33
|
+
)
|
|
34
|
+
from latticeai.services.model_engines import (
|
|
35
|
+
engine_support_status as _engine_support_status,
|
|
36
|
+
)
|
|
37
|
+
from latticeai.services.model_engines import (
|
|
38
|
+
ensure_llamacpp_server as _ensure_llamacpp_server,
|
|
39
|
+
)
|
|
40
|
+
from latticeai.services.model_engines import (
|
|
41
|
+
ensure_lmstudio_server as _ensure_lmstudio_server,
|
|
42
|
+
)
|
|
43
|
+
from latticeai.services.model_engines import (
|
|
44
|
+
ensure_ollama_server as _ensure_ollama_server,
|
|
45
|
+
)
|
|
46
|
+
from latticeai.services.model_engines import (
|
|
47
|
+
ensure_vllm_server as _ensure_vllm_server,
|
|
48
|
+
)
|
|
49
|
+
from latticeai.services.model_engines import (
|
|
50
|
+
find_lmstudio_cli as _find_lmstudio_cli,
|
|
51
|
+
)
|
|
52
|
+
from latticeai.services.model_engines import (
|
|
53
|
+
get_ollama_pulled_models as _get_ollama_pulled_models,
|
|
54
|
+
)
|
|
55
|
+
from latticeai.services.model_engines import (
|
|
56
|
+
get_openai_compatible_server_models as _get_openai_compatible_server_models,
|
|
57
|
+
)
|
|
58
|
+
from latticeai.services.model_engines import (
|
|
59
|
+
local_binary as _local_binary,
|
|
60
|
+
)
|
|
61
|
+
from latticeai.services.model_engines import (
|
|
62
|
+
pull_ollama_model_with_progress as _pull_ollama_model_with_progress,
|
|
63
|
+
)
|
|
64
|
+
from latticeai.services.model_engines import (
|
|
65
|
+
vllm_executable as _vllm_executable,
|
|
66
|
+
)
|
|
67
|
+
from latticeai.services.model_engines import (
|
|
68
|
+
vllm_metal_python as _vllm_metal_python,
|
|
69
|
+
)
|
|
70
|
+
from latticeai.services.model_engines import (
|
|
71
|
+
wait_for_openai_compatible_server as _wait_for_openai_compatible_server,
|
|
72
|
+
)
|
|
73
|
+
from latticeai.services.model_engines import (
|
|
74
|
+
windows_binary_candidates as _windows_binary_candidates,
|
|
75
|
+
)
|
|
76
|
+
from latticeai.services.model_errors import ModelRuntimeError
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _update_env_file(env_file: Path, key: str, value: str) -> None:
|
|
80
|
+
lines = []
|
|
81
|
+
found = False
|
|
82
|
+
if env_file.exists():
|
|
83
|
+
for line in env_file.read_text(encoding="utf-8").splitlines():
|
|
84
|
+
if line.startswith(f"{key}="):
|
|
85
|
+
lines.append(f"{key}={value}")
|
|
86
|
+
found = True
|
|
87
|
+
else:
|
|
88
|
+
lines.append(line)
|
|
89
|
+
if not found:
|
|
90
|
+
lines.append(f"{key}={value}")
|
|
91
|
+
env_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
LOCAL_SERVER_PROCESSES = _LOCAL_SERVER_PROCESSES
|
|
95
|
+
VLLM_METAL_ENV = Path.home() / ".venv-vllm-metal"
|
|
96
|
+
VLLM_METAL_BIN = VLLM_METAL_ENV / "bin" / "vllm"
|
|
97
|
+
VLLM_METAL_PYTHON = VLLM_METAL_ENV / "bin" / "python"
|
|
98
|
+
LMSTUDIO_BUNDLED_CLI = Path("/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms")
|
|
99
|
+
|
|
100
|
+
def windows_binary_candidates(binary: str) -> List[Path]:
|
|
101
|
+
return _windows_binary_candidates(binary)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def local_binary(binary: str) -> Optional[str]:
|
|
105
|
+
return _local_binary(binary)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def find_lmstudio_cli() -> Optional[str]:
|
|
109
|
+
return _find_lmstudio_cli()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def vllm_executable() -> Optional[str]:
|
|
113
|
+
return _vllm_executable()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def vllm_metal_python() -> Optional[str]:
|
|
117
|
+
return _vllm_metal_python()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _json_request(
|
|
121
|
+
url: str,
|
|
122
|
+
*,
|
|
123
|
+
method: str = "GET",
|
|
124
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
125
|
+
headers: Optional[Dict[str, str]] = None,
|
|
126
|
+
timeout: float = 10.0,
|
|
127
|
+
) -> Dict[str, Any]:
|
|
128
|
+
data = None
|
|
129
|
+
req_headers = dict(headers or {})
|
|
130
|
+
if payload is not None:
|
|
131
|
+
data = json.dumps(payload).encode("utf-8")
|
|
132
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
133
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
|
|
134
|
+
with urllib.request.urlopen(req, timeout=timeout) as res:
|
|
135
|
+
raw = res.read().decode("utf-8", errors="replace")
|
|
136
|
+
if not raw.strip():
|
|
137
|
+
return {}
|
|
138
|
+
return json.loads(raw)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def lmstudio_api_base() -> str:
|
|
142
|
+
return (os.getenv("LMSTUDIO_BASE_URL") or OPENAI_COMPATIBLE_PROVIDERS["lmstudio"]["base_url"]).rstrip("/")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def lmstudio_native_api_base() -> str:
|
|
146
|
+
base = lmstudio_api_base()
|
|
147
|
+
return base[:-3] if base.endswith("/v1") else base
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ensure_lmstudio_server() -> None:
|
|
151
|
+
return _ensure_lmstudio_server()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_LMSTUDIO_MODELS_CACHE: List[Dict[str, Any]] = []
|
|
155
|
+
_LMSTUDIO_MODELS_CACHE_TS: float = 0.0
|
|
156
|
+
_LMSTUDIO_MODELS_CACHE_TTL: float = 10.0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_lmstudio_models(*, force: bool = False) -> List[Dict[str, Any]]:
|
|
160
|
+
global _LMSTUDIO_MODELS_CACHE, _LMSTUDIO_MODELS_CACHE_TS
|
|
161
|
+
if not force and time.monotonic() - _LMSTUDIO_MODELS_CACHE_TS < _LMSTUDIO_MODELS_CACHE_TTL:
|
|
162
|
+
return _LMSTUDIO_MODELS_CACHE
|
|
163
|
+
try:
|
|
164
|
+
payload = _json_request(
|
|
165
|
+
f"{lmstudio_native_api_base()}/api/v1/models",
|
|
166
|
+
headers={"Authorization": f"Bearer {os.getenv('LMSTUDIO_API_KEY') or 'lmstudio'}"},
|
|
167
|
+
timeout=2.5,
|
|
168
|
+
)
|
|
169
|
+
except Exception:
|
|
170
|
+
return _LMSTUDIO_MODELS_CACHE
|
|
171
|
+
models = payload.get("models")
|
|
172
|
+
_LMSTUDIO_MODELS_CACHE = models if isinstance(models, list) else []
|
|
173
|
+
_LMSTUDIO_MODELS_CACHE_TS = time.monotonic()
|
|
174
|
+
return _LMSTUDIO_MODELS_CACHE
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _lmstudio_candidate_keys(model_name: str) -> List[str]:
|
|
178
|
+
raw = model_name.strip()
|
|
179
|
+
if not raw:
|
|
180
|
+
return []
|
|
181
|
+
slug = raw.split("/")[-1].lower()
|
|
182
|
+
slug = slug.replace("-gguf", "").replace("-awq", "")
|
|
183
|
+
parts = [p for p in slug.split("-") if p]
|
|
184
|
+
candidates = [raw.lower(), slug]
|
|
185
|
+
if parts:
|
|
186
|
+
candidates.append("-".join(parts[: min(4, len(parts))]))
|
|
187
|
+
return list(dict.fromkeys(candidates))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _find_lmstudio_model_key(model_name: str, models: List[Dict[str, Any]]) -> Optional[str]:
|
|
191
|
+
if not models:
|
|
192
|
+
return None
|
|
193
|
+
candidate_keys = _lmstudio_candidate_keys(model_name)
|
|
194
|
+
exact = []
|
|
195
|
+
fuzzy = []
|
|
196
|
+
for item in models:
|
|
197
|
+
key = str(item.get("key") or "").strip()
|
|
198
|
+
display_name = str(item.get("display_name") or "").strip()
|
|
199
|
+
haystacks = [key.lower(), display_name.lower()]
|
|
200
|
+
if any(raw == key.lower() for raw in candidate_keys):
|
|
201
|
+
exact.append(key)
|
|
202
|
+
continue
|
|
203
|
+
if any(token and token in hay for token in candidate_keys for hay in haystacks):
|
|
204
|
+
fuzzy.append(key)
|
|
205
|
+
return next(iter(exact or fuzzy), None)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def ensure_lmstudio_model(model_name: str) -> Dict[str, Any]:
|
|
209
|
+
ensure_lmstudio_server()
|
|
210
|
+
auth_header = {"Authorization": f"Bearer {os.getenv('LMSTUDIO_API_KEY') or 'lmstudio'}"}
|
|
211
|
+
models = get_lmstudio_models()
|
|
212
|
+
found_key = _find_lmstudio_model_key(model_name, models)
|
|
213
|
+
model_key = found_key or model_name
|
|
214
|
+
|
|
215
|
+
if not found_key:
|
|
216
|
+
try:
|
|
217
|
+
job = _json_request(
|
|
218
|
+
f"{lmstudio_native_api_base()}/api/v1/models/download",
|
|
219
|
+
method="POST",
|
|
220
|
+
payload={"model": model_name},
|
|
221
|
+
headers=auth_header,
|
|
222
|
+
timeout=30,
|
|
223
|
+
)
|
|
224
|
+
except urllib.error.HTTPError as e:
|
|
225
|
+
detail = e.read().decode("utf-8", errors="replace")[-2000:]
|
|
226
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {detail or e.reason}")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {e}")
|
|
229
|
+
|
|
230
|
+
status = str(job.get("status") or "")
|
|
231
|
+
job_id = str(job.get("job_id") or "")
|
|
232
|
+
if status not in {"completed", "already_downloaded"} and job_id:
|
|
233
|
+
deadline = time.time() + 3600
|
|
234
|
+
while time.time() < deadline:
|
|
235
|
+
polled = _json_request(
|
|
236
|
+
f"{lmstudio_native_api_base()}/api/v1/models/download/status/{job_id}",
|
|
237
|
+
headers=auth_header,
|
|
238
|
+
timeout=30,
|
|
239
|
+
)
|
|
240
|
+
polled_status = str(polled.get("status") or "")
|
|
241
|
+
if polled_status == "completed":
|
|
242
|
+
break
|
|
243
|
+
if polled_status == "failed":
|
|
244
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {polled}")
|
|
245
|
+
time.sleep(2)
|
|
246
|
+
else:
|
|
247
|
+
raise ModelRuntimeError(status_code=408, detail="LM Studio 모델 다운로드 시간이 초과되었습니다.")
|
|
248
|
+
|
|
249
|
+
models = get_lmstudio_models(force=True)
|
|
250
|
+
model_key = _find_lmstudio_model_key(model_name, models) or model_name
|
|
251
|
+
|
|
252
|
+
target = next((item for item in models if isinstance(item, dict) and item.get("key") == model_key), None)
|
|
253
|
+
loaded_instances = target.get("loaded_instances") if isinstance(target, dict) else None
|
|
254
|
+
if loaded_instances:
|
|
255
|
+
return {"provider": "lmstudio", "model": model_name, "resolved_model": model_key, "server_ready": True, "cached": True}
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
loaded = _json_request(
|
|
259
|
+
f"{lmstudio_native_api_base()}/api/v1/models/load",
|
|
260
|
+
method="POST",
|
|
261
|
+
payload={"model": model_key, "context_length": 4096},
|
|
262
|
+
headers=auth_header,
|
|
263
|
+
timeout=120,
|
|
264
|
+
)
|
|
265
|
+
except urllib.error.HTTPError as e:
|
|
266
|
+
detail = e.read().decode("utf-8", errors="replace")[-2000:]
|
|
267
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {detail or e.reason}")
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {e}")
|
|
270
|
+
|
|
271
|
+
if str(loaded.get("status") or "") != "loaded":
|
|
272
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {loaded}")
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"provider": "lmstudio",
|
|
276
|
+
"model": model_name,
|
|
277
|
+
"resolved_model": model_key,
|
|
278
|
+
"instance_id": loaded.get("instance_id"),
|
|
279
|
+
"server_ready": True,
|
|
280
|
+
"cached": False,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def engine_support_status(engine: str) -> Dict[str, Any]:
|
|
284
|
+
return _engine_support_status(engine)
|
|
285
|
+
|
|
286
|
+
def pull_ollama_model_with_progress(model_name: str, progress_emit=None) -> Dict[str, Any]:
|
|
287
|
+
return _pull_ollama_model_with_progress(model_name, progress_emit)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_ollama_pulled_models() -> set:
|
|
291
|
+
return _get_ollama_pulled_models()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_openai_compatible_server_models(provider: str) -> List[str]:
|
|
295
|
+
return _get_openai_compatible_server_models(provider)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def ensure_ollama_server() -> None:
|
|
299
|
+
return _ensure_ollama_server()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def wait_for_openai_compatible_server(provider: str, model_name: Optional[str] = None, timeout: int = 45) -> bool:
|
|
303
|
+
return _wait_for_openai_compatible_server(provider, model_name=model_name, timeout=timeout)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def ensure_vllm_server(model_name: str) -> None:
|
|
307
|
+
return _ensure_vllm_server(model_name)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def ensure_llamacpp_server(model_name: str) -> None:
|
|
311
|
+
return _ensure_llamacpp_server(model_name)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _safe_engine_install_plan(
|
|
315
|
+
engine: str,
|
|
316
|
+
*,
|
|
317
|
+
base_dir: Path,
|
|
318
|
+
) -> Optional[Dict[str, Any]]:
|
|
319
|
+
try:
|
|
320
|
+
return _engine_install_plan(engine, base_dir=base_dir)
|
|
321
|
+
except Exception:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def engine_installed(engine: str) -> bool:
|
|
326
|
+
if engine == "local_mlx":
|
|
327
|
+
return bool(
|
|
328
|
+
importlib.util.find_spec("mlx")
|
|
329
|
+
and (importlib.util.find_spec("mlx_vlm") or importlib.util.find_spec("mlx_lm"))
|
|
330
|
+
)
|
|
331
|
+
if engine == "ollama":
|
|
332
|
+
return local_binary("ollama") is not None
|
|
333
|
+
if engine == "vllm":
|
|
334
|
+
return vllm_metal_python() is not None or vllm_executable() is not None or importlib.util.find_spec("vllm") is not None
|
|
335
|
+
if engine == "lmstudio":
|
|
336
|
+
return find_lmstudio_cli() is not None or Path("/Applications/LM Studio.app").exists()
|
|
337
|
+
if engine == "llamacpp":
|
|
338
|
+
return shutil.which("llama-server") is not None
|
|
339
|
+
if engine in {"openai", "openrouter", "groq", "together", "xai"}:
|
|
340
|
+
return AsyncOpenAI is not None
|
|
341
|
+
return False
|