lemonade-sdk 8.1.4__py3-none-any.whl → 8.2.2__py3-none-any.whl
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.
Potentially problematic release.
This version of lemonade-sdk might be problematic. Click here for more details.
- lemonade/cache.py +6 -1
- lemonade/cli.py +47 -5
- lemonade/common/inference_engines.py +13 -4
- lemonade/common/status.py +4 -4
- lemonade/common/system_info.py +544 -1
- lemonade/profilers/agt_power.py +437 -0
- lemonade/profilers/hwinfo_power.py +429 -0
- lemonade/tools/accuracy.py +143 -48
- lemonade/tools/adapter.py +6 -1
- lemonade/tools/bench.py +26 -8
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +303 -0
- lemonade/tools/huggingface/bench.py +6 -1
- lemonade/tools/llamacpp/bench.py +146 -27
- lemonade/tools/llamacpp/load.py +30 -2
- lemonade/tools/llamacpp/utils.py +393 -33
- lemonade/tools/oga/bench.py +5 -26
- lemonade/tools/oga/load.py +60 -121
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/report/table.py +76 -8
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +220 -553
- lemonade/tools/server/serve.py +684 -168
- lemonade/tools/server/static/js/chat.js +666 -342
- lemonade/tools/server/static/js/model-settings.js +24 -3
- lemonade/tools/server/static/js/models.js +597 -73
- lemonade/tools/server/static/js/shared.js +79 -14
- lemonade/tools/server/static/logs.html +191 -0
- lemonade/tools/server/static/styles.css +491 -66
- lemonade/tools/server/static/webapp.html +83 -31
- lemonade/tools/server/tray.py +158 -38
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/{system_tray.py → windows_tray.py} +13 -0
- lemonade/tools/server/webapp.py +4 -1
- lemonade/tools/server/wrapped_server.py +559 -0
- lemonade/version.py +1 -1
- lemonade_install/install.py +54 -611
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/METADATA +29 -72
- lemonade_sdk-8.2.2.dist-info/RECORD +83 -0
- lemonade_server/cli.py +145 -37
- lemonade_server/model_manager.py +521 -37
- lemonade_server/pydantic_models.py +28 -1
- lemonade_server/server_models.json +246 -92
- lemonade_server/settings.py +39 -39
- lemonade/tools/quark/__init__.py +0 -0
- lemonade/tools/quark/quark_load.py +0 -173
- lemonade/tools/quark/quark_quantize.py +0 -439
- lemonade_sdk-8.1.4.dist-info/RECORD +0 -77
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/top_level.txt +0 -0
lemonade_server/model_manager.py
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import subprocess
|
|
3
4
|
from typing import Optional
|
|
4
5
|
import shutil
|
|
5
6
|
import huggingface_hub
|
|
6
7
|
from importlib.metadata import distributions
|
|
7
8
|
from lemonade_server.pydantic_models import PullConfig
|
|
9
|
+
from lemonade_server.pydantic_models import PullConfig
|
|
8
10
|
from lemonade.cache import DEFAULT_CACHE_DIR
|
|
9
|
-
from lemonade.tools.llamacpp.utils import
|
|
11
|
+
from lemonade.tools.llamacpp.utils import (
|
|
12
|
+
parse_checkpoint,
|
|
13
|
+
download_gguf,
|
|
14
|
+
resolve_local_gguf_model,
|
|
15
|
+
)
|
|
10
16
|
from lemonade.common.network import custom_snapshot_download
|
|
17
|
+
from lemonade.tools.oga.migration import (
|
|
18
|
+
detect_incompatible_ryzenai_models,
|
|
19
|
+
delete_incompatible_models,
|
|
20
|
+
)
|
|
11
21
|
|
|
12
22
|
USER_MODELS_FILE = os.path.join(DEFAULT_CACHE_DIR, "user_models.json")
|
|
13
23
|
|
|
24
|
+
from lemonade.tools.flm.utils import (
|
|
25
|
+
get_flm_installed_models,
|
|
26
|
+
is_flm_available,
|
|
27
|
+
install_flm,
|
|
28
|
+
download_flm_model,
|
|
29
|
+
)
|
|
30
|
+
|
|
14
31
|
|
|
15
32
|
class ModelManager:
|
|
16
33
|
|
|
@@ -52,10 +69,17 @@ class ModelManager:
|
|
|
52
69
|
|
|
53
70
|
# Add the model name as a key in each entry, to make it easier
|
|
54
71
|
# to access later
|
|
55
|
-
|
|
72
|
+
# Also convert labels to boolean fields for LoadConfig compatibility
|
|
56
73
|
for key, value in models.items():
|
|
57
74
|
value["model_name"] = key
|
|
58
75
|
|
|
76
|
+
# Convert labels to boolean fields for backwards compatibility with LoadConfig
|
|
77
|
+
labels = value.get("labels", [])
|
|
78
|
+
if "reasoning" in labels and "reasoning" not in value:
|
|
79
|
+
value["reasoning"] = True
|
|
80
|
+
if "vision" in labels and "vision" not in value:
|
|
81
|
+
value["vision"] = True
|
|
82
|
+
|
|
59
83
|
return models
|
|
60
84
|
|
|
61
85
|
@property
|
|
@@ -77,17 +101,131 @@ class ModelManager:
|
|
|
77
101
|
def downloaded_models(self) -> dict:
|
|
78
102
|
"""
|
|
79
103
|
Returns a dictionary of locally available models.
|
|
104
|
+
For GGUF models with variants, checks if the specific variant files exist.
|
|
80
105
|
"""
|
|
106
|
+
from huggingface_hub.constants import HF_HUB_CACHE
|
|
107
|
+
|
|
81
108
|
downloaded_models = {}
|
|
82
109
|
downloaded_checkpoints = self.downloaded_hf_checkpoints
|
|
110
|
+
|
|
111
|
+
# Get FLM installed model checkpoints
|
|
112
|
+
flm_installed_checkpoints = get_flm_installed_models()
|
|
113
|
+
|
|
83
114
|
for model in self.supported_models:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if
|
|
88
|
-
|
|
115
|
+
model_info = self.supported_models[model]
|
|
116
|
+
|
|
117
|
+
# Handle FLM models
|
|
118
|
+
if model_info.get("recipe") == "flm":
|
|
119
|
+
if model_info["checkpoint"] in flm_installed_checkpoints:
|
|
120
|
+
downloaded_models[model] = model_info
|
|
121
|
+
else:
|
|
122
|
+
# Handle other models
|
|
123
|
+
checkpoint = model_info["checkpoint"]
|
|
124
|
+
base_checkpoint, variant = parse_checkpoint(checkpoint)
|
|
125
|
+
|
|
126
|
+
# Special handling for locally uploaded user models (not internet-downloaded)
|
|
127
|
+
if (
|
|
128
|
+
model.startswith("user.")
|
|
129
|
+
and model_info.get("source") == "local_upload"
|
|
130
|
+
):
|
|
131
|
+
# Locally uploaded model: checkpoint is in cache directory format (models--xxx)
|
|
132
|
+
local_model_path = os.path.join(HF_HUB_CACHE, base_checkpoint)
|
|
133
|
+
if os.path.exists(local_model_path):
|
|
134
|
+
downloaded_models[model] = model_info
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# For all other models (server models and internet-downloaded user models),
|
|
138
|
+
# use the standard verification logic with variant checks
|
|
139
|
+
if base_checkpoint in downloaded_checkpoints:
|
|
140
|
+
# For GGUF models with variants, verify the specific variant files exist
|
|
141
|
+
if variant and model_info.get("recipe") == "llamacpp":
|
|
142
|
+
try:
|
|
143
|
+
from lemonade.tools.llamacpp.utils import (
|
|
144
|
+
identify_gguf_models,
|
|
145
|
+
)
|
|
146
|
+
from lemonade.common.network import custom_snapshot_download
|
|
147
|
+
|
|
148
|
+
# Get the local snapshot path
|
|
149
|
+
snapshot_path = custom_snapshot_download(
|
|
150
|
+
base_checkpoint, local_files_only=True
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check if the specific variant files exist
|
|
154
|
+
core_files, sharded_files = identify_gguf_models(
|
|
155
|
+
base_checkpoint, variant, model_info.get("mmproj", "")
|
|
156
|
+
)
|
|
157
|
+
all_variant_files = (
|
|
158
|
+
list(core_files.values()) + sharded_files
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Verify all required files exist locally
|
|
162
|
+
all_files_exist = True
|
|
163
|
+
for file_path in all_variant_files:
|
|
164
|
+
full_file_path = os.path.join(snapshot_path, file_path)
|
|
165
|
+
if not os.path.exists(full_file_path):
|
|
166
|
+
all_files_exist = False
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
if all_files_exist:
|
|
170
|
+
downloaded_models[model] = model_info
|
|
171
|
+
|
|
172
|
+
except Exception:
|
|
173
|
+
# If we can't verify the variant, don't include it
|
|
174
|
+
pass
|
|
175
|
+
else:
|
|
176
|
+
# For non-GGUF models or GGUF without variants, use the original logic
|
|
177
|
+
downloaded_models[model] = model_info
|
|
89
178
|
return downloaded_models
|
|
90
179
|
|
|
180
|
+
def register_local_model(
|
|
181
|
+
self,
|
|
182
|
+
model_name: str,
|
|
183
|
+
checkpoint: str,
|
|
184
|
+
recipe: str,
|
|
185
|
+
reasoning: bool = False,
|
|
186
|
+
vision: bool = False,
|
|
187
|
+
mmproj: str = "",
|
|
188
|
+
snapshot_path: str = "",
|
|
189
|
+
):
|
|
190
|
+
|
|
191
|
+
model_name_clean = model_name[5:]
|
|
192
|
+
|
|
193
|
+
# Prepare model info
|
|
194
|
+
labels = ["custom"]
|
|
195
|
+
if reasoning:
|
|
196
|
+
labels.append("reasoning")
|
|
197
|
+
if vision:
|
|
198
|
+
labels.append("vision")
|
|
199
|
+
|
|
200
|
+
new_user_model = {
|
|
201
|
+
"checkpoint": checkpoint,
|
|
202
|
+
"recipe": recipe,
|
|
203
|
+
"suggested": True,
|
|
204
|
+
"labels": labels,
|
|
205
|
+
"source": "local_upload",
|
|
206
|
+
}
|
|
207
|
+
if mmproj:
|
|
208
|
+
new_user_model["mmproj"] = mmproj
|
|
209
|
+
|
|
210
|
+
# Load existing user models
|
|
211
|
+
user_models = {}
|
|
212
|
+
if os.path.exists(USER_MODELS_FILE):
|
|
213
|
+
with open(USER_MODELS_FILE, "r", encoding="utf-8") as file:
|
|
214
|
+
user_models = json.load(file)
|
|
215
|
+
|
|
216
|
+
# Check for conflicts
|
|
217
|
+
if model_name_clean in user_models:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"{model_name_clean} is already registered."
|
|
220
|
+
f"Please use a different model name or delete the existing model."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Save to user_models.json
|
|
224
|
+
user_models[model_name_clean] = new_user_model
|
|
225
|
+
os.makedirs(os.path.dirname(USER_MODELS_FILE), exist_ok=True)
|
|
226
|
+
with open(USER_MODELS_FILE, "w", encoding="utf-8") as file:
|
|
227
|
+
json.dump(user_models, file)
|
|
228
|
+
|
|
91
229
|
@property
|
|
92
230
|
def downloaded_models_enabled(self) -> dict:
|
|
93
231
|
"""
|
|
@@ -102,6 +240,7 @@ class ModelManager:
|
|
|
102
240
|
checkpoint: Optional[str] = None,
|
|
103
241
|
recipe: Optional[str] = None,
|
|
104
242
|
reasoning: bool = False,
|
|
243
|
+
vision: bool = False,
|
|
105
244
|
mmproj: str = "",
|
|
106
245
|
do_not_upgrade: bool = False,
|
|
107
246
|
):
|
|
@@ -115,6 +254,7 @@ class ModelManager:
|
|
|
115
254
|
if model not in self.supported_models:
|
|
116
255
|
# Register the model as a user model if the model name
|
|
117
256
|
# is not already registered
|
|
257
|
+
import logging
|
|
118
258
|
|
|
119
259
|
# Ensure the model name includes the `user` namespace
|
|
120
260
|
model_parsed = model.split(".", 1)
|
|
@@ -137,11 +277,17 @@ class ModelManager:
|
|
|
137
277
|
)
|
|
138
278
|
|
|
139
279
|
# JSON content that will be used for registration if the download succeeds
|
|
280
|
+
labels = ["custom"]
|
|
281
|
+
if reasoning:
|
|
282
|
+
labels.append("reasoning")
|
|
283
|
+
if vision:
|
|
284
|
+
labels.append("vision")
|
|
285
|
+
|
|
140
286
|
new_user_model = {
|
|
141
287
|
"checkpoint": checkpoint,
|
|
142
288
|
"recipe": recipe,
|
|
143
289
|
"suggested": True,
|
|
144
|
-
"labels":
|
|
290
|
+
"labels": labels,
|
|
145
291
|
}
|
|
146
292
|
|
|
147
293
|
if mmproj:
|
|
@@ -164,8 +310,64 @@ class ModelManager:
|
|
|
164
310
|
checkpoint=checkpoint,
|
|
165
311
|
recipe=recipe,
|
|
166
312
|
reasoning=reasoning,
|
|
313
|
+
vision=vision,
|
|
167
314
|
)
|
|
168
315
|
else:
|
|
316
|
+
# Model is already registered - check if trying to register with different parameters
|
|
317
|
+
existing_model = self.supported_models[model]
|
|
318
|
+
existing_checkpoint = existing_model.get("checkpoint")
|
|
319
|
+
existing_recipe = existing_model.get("recipe")
|
|
320
|
+
existing_reasoning = "reasoning" in existing_model.get("labels", [])
|
|
321
|
+
existing_mmproj = existing_model.get("mmproj", "")
|
|
322
|
+
existing_vision = "vision" in existing_model.get("labels", [])
|
|
323
|
+
|
|
324
|
+
# Compare parameters
|
|
325
|
+
checkpoint_differs = checkpoint and checkpoint != existing_checkpoint
|
|
326
|
+
recipe_differs = recipe and recipe != existing_recipe
|
|
327
|
+
reasoning_differs = reasoning and reasoning != existing_reasoning
|
|
328
|
+
mmproj_differs = mmproj and mmproj != existing_mmproj
|
|
329
|
+
vision_differs = vision and vision != existing_vision
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
checkpoint_differs
|
|
333
|
+
or recipe_differs
|
|
334
|
+
or reasoning_differs
|
|
335
|
+
or mmproj_differs
|
|
336
|
+
or vision_differs
|
|
337
|
+
):
|
|
338
|
+
conflicts = []
|
|
339
|
+
if checkpoint_differs:
|
|
340
|
+
conflicts.append(
|
|
341
|
+
f"checkpoint (existing: '{existing_checkpoint}', new: '{checkpoint}')"
|
|
342
|
+
)
|
|
343
|
+
if recipe_differs:
|
|
344
|
+
conflicts.append(
|
|
345
|
+
f"recipe (existing: '{existing_recipe}', new: '{recipe}')"
|
|
346
|
+
)
|
|
347
|
+
if reasoning_differs:
|
|
348
|
+
conflicts.append(
|
|
349
|
+
f"reasoning (existing: {existing_reasoning}, new: {reasoning})"
|
|
350
|
+
)
|
|
351
|
+
if mmproj_differs:
|
|
352
|
+
conflicts.append(
|
|
353
|
+
f"mmproj (existing: '{existing_mmproj}', new: '{mmproj}')"
|
|
354
|
+
)
|
|
355
|
+
if vision_differs:
|
|
356
|
+
conflicts.append(
|
|
357
|
+
f"vision (existing: {existing_vision}, new: {vision})"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
conflict_details = ", ".join(conflicts)
|
|
361
|
+
|
|
362
|
+
additional_suggestion = ""
|
|
363
|
+
if model.startswith("user."):
|
|
364
|
+
additional_suggestion = f" or delete the existing model first using `lemonade-server delete {model}`"
|
|
365
|
+
|
|
366
|
+
raise ValueError(
|
|
367
|
+
f"Model {model} is already registered with a different configuration. "
|
|
368
|
+
f"Conflicting parameters: {conflict_details}. "
|
|
369
|
+
f"Please use a different model name{additional_suggestion}."
|
|
370
|
+
)
|
|
169
371
|
new_registration_model_config = None
|
|
170
372
|
|
|
171
373
|
# Download the model
|
|
@@ -177,12 +379,51 @@ class ModelManager:
|
|
|
177
379
|
gguf_model_config = PullConfig(**self.supported_models[model])
|
|
178
380
|
print(f"Downloading {model} ({checkpoint_to_download})")
|
|
179
381
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
382
|
+
# Handle FLM models
|
|
383
|
+
current_recipe = (
|
|
384
|
+
recipe
|
|
385
|
+
if new_registration_model_config
|
|
386
|
+
else self.supported_models[model].get("recipe")
|
|
387
|
+
)
|
|
388
|
+
if current_recipe == "flm":
|
|
389
|
+
# Check if FLM is available, and install it if not
|
|
390
|
+
if not is_flm_available():
|
|
391
|
+
print(
|
|
392
|
+
"FLM is not installed or not at the latest version. Installing FLM..."
|
|
393
|
+
)
|
|
394
|
+
install_flm()
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
download_flm_model(checkpoint_to_download, None, do_not_upgrade)
|
|
398
|
+
print(f"Successfully downloaded FLM model: {model}")
|
|
399
|
+
except subprocess.CalledProcessError as e:
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
f"Failed to download FLM model {model}: {e}"
|
|
402
|
+
) from e
|
|
403
|
+
except FileNotFoundError as e:
|
|
404
|
+
# This shouldn't happen after install_flm(), but just in case
|
|
405
|
+
raise RuntimeError(
|
|
406
|
+
f"FLM command not found even after installation attempt. "
|
|
407
|
+
f"Please manually install FLM using 'lemonade-install --flm'."
|
|
408
|
+
) from e
|
|
409
|
+
elif "gguf" in checkpoint_to_download.lower():
|
|
410
|
+
# Parse checkpoint to check local cache first
|
|
411
|
+
base_checkpoint, variant = parse_checkpoint(
|
|
412
|
+
gguf_model_config.checkpoint
|
|
413
|
+
)
|
|
414
|
+
local_result = resolve_local_gguf_model(
|
|
415
|
+
base_checkpoint, variant, gguf_model_config.mmproj
|
|
185
416
|
)
|
|
417
|
+
|
|
418
|
+
# Only download if not found locally
|
|
419
|
+
if not local_result:
|
|
420
|
+
download_gguf(
|
|
421
|
+
gguf_model_config.checkpoint,
|
|
422
|
+
gguf_model_config.mmproj,
|
|
423
|
+
do_not_upgrade=do_not_upgrade,
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
print(f"Model already exists locally, skipping download")
|
|
186
427
|
else:
|
|
187
428
|
custom_snapshot_download(
|
|
188
429
|
checkpoint_to_download, do_not_upgrade=do_not_upgrade
|
|
@@ -192,6 +433,11 @@ class ModelManager:
|
|
|
192
433
|
# We do this registration after the download so that we don't register
|
|
193
434
|
# any incorrectly configured models where the download would fail
|
|
194
435
|
if new_registration_model_config:
|
|
436
|
+
# For models downloaded from the internet (HuggingFace),
|
|
437
|
+
# keep the original checkpoint format (e.g., "amd/Llama-3.2-1B-Instruct-...")
|
|
438
|
+
# Do NOT convert to cache directory format - that's only for locally uploaded models
|
|
439
|
+
new_user_model["checkpoint"] = checkpoint
|
|
440
|
+
|
|
195
441
|
if os.path.exists(USER_MODELS_FILE):
|
|
196
442
|
with open(USER_MODELS_FILE, "r", encoding="utf-8") as file:
|
|
197
443
|
user_models: dict = json.load(file)
|
|
@@ -209,66 +455,304 @@ class ModelManager:
|
|
|
209
455
|
def filter_models_by_backend(self, models: dict) -> dict:
|
|
210
456
|
"""
|
|
211
457
|
Returns a filtered dict of models that are enabled by the
|
|
212
|
-
current environment.
|
|
458
|
+
current environment and platform.
|
|
213
459
|
"""
|
|
460
|
+
import platform
|
|
461
|
+
|
|
214
462
|
installed_packages = {dist.metadata["Name"].lower() for dist in distributions()}
|
|
215
463
|
|
|
216
|
-
|
|
464
|
+
ryzenai_installed = (
|
|
217
465
|
"onnxruntime-vitisai" in installed_packages
|
|
218
466
|
and "onnxruntime-genai-directml-ryzenai" in installed_packages
|
|
219
467
|
)
|
|
468
|
+
|
|
469
|
+
from lemonade_install.install import (
|
|
470
|
+
check_ryzen_ai_processor,
|
|
471
|
+
UnsupportedPlatformError,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
check_ryzen_ai_processor()
|
|
476
|
+
ryzenai_npu_available = True
|
|
477
|
+
except UnsupportedPlatformError:
|
|
478
|
+
ryzenai_npu_available = False
|
|
479
|
+
|
|
480
|
+
# On macOS, only llamacpp (GGUF) models are supported, and only on Apple Silicon with macOS 14+
|
|
481
|
+
is_macos = platform.system() == "Darwin"
|
|
482
|
+
if is_macos:
|
|
483
|
+
machine = platform.machine().lower()
|
|
484
|
+
if machine == "x86_64":
|
|
485
|
+
# Intel Macs are not supported - return empty model list with error info
|
|
486
|
+
return {
|
|
487
|
+
"_unsupported_platform_error": {
|
|
488
|
+
"error": "Intel Mac Not Supported",
|
|
489
|
+
"message": (
|
|
490
|
+
"Lemonade Server requires Apple Silicon processors on macOS. "
|
|
491
|
+
"Intel Macs are not currently supported. "
|
|
492
|
+
"Please use a Mac with Apple Silicon or try Lemonade on Windows/Linux."
|
|
493
|
+
),
|
|
494
|
+
"platform": f"macOS {machine}",
|
|
495
|
+
"supported": "macOS 14+ with Apple Silicon (arm64/aarch64)",
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Check macOS version requirement
|
|
500
|
+
mac_version = platform.mac_ver()[0]
|
|
501
|
+
if mac_version:
|
|
502
|
+
major_version = int(mac_version.split(".")[0])
|
|
503
|
+
if major_version < 14:
|
|
504
|
+
return {
|
|
505
|
+
"_unsupported_platform_error": {
|
|
506
|
+
"error": "macOS Version Not Supported",
|
|
507
|
+
"message": (
|
|
508
|
+
f"Lemonade Server requires macOS 14 or later. "
|
|
509
|
+
f"Your system is running macOS {mac_version}. "
|
|
510
|
+
f"Please update your macOS version to use Lemonade Server."
|
|
511
|
+
),
|
|
512
|
+
"platform": f"macOS {mac_version} {machine}",
|
|
513
|
+
"supported": "macOS 14+ with Apple Silicon (arm64/aarch64)",
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
220
517
|
filtered = {}
|
|
221
518
|
for model, value in models.items():
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
519
|
+
recipe = value.get("recipe")
|
|
520
|
+
|
|
521
|
+
# Filter Ryzen AI models based on package availability
|
|
522
|
+
if recipe == "oga-hybrid" or recipe == "oga-npu":
|
|
523
|
+
if not ryzenai_installed:
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
if recipe == "flm":
|
|
527
|
+
if not ryzenai_npu_available:
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
# On macOS, only show llamacpp models (GGUF format)
|
|
531
|
+
if is_macos and recipe != "llamacpp":
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
filtered[model] = value
|
|
535
|
+
|
|
227
536
|
return filtered
|
|
228
537
|
|
|
229
538
|
def delete_model(self, model_name: str):
|
|
230
539
|
"""
|
|
231
540
|
Deletes the specified model from local storage.
|
|
541
|
+
For GGUF models with variants, only deletes the specific variant files.
|
|
232
542
|
"""
|
|
543
|
+
from huggingface_hub.constants import HF_HUB_CACHE
|
|
544
|
+
|
|
233
545
|
if model_name not in self.supported_models:
|
|
234
546
|
raise ValueError(
|
|
235
547
|
f"Model {model_name} is not supported. Please choose from the following: "
|
|
236
548
|
f"{list(self.supported_models.keys())}"
|
|
237
549
|
)
|
|
238
550
|
|
|
239
|
-
|
|
551
|
+
model_info = self.supported_models[model_name]
|
|
552
|
+
checkpoint = model_info["checkpoint"]
|
|
240
553
|
print(f"Deleting {model_name} ({checkpoint})")
|
|
241
554
|
|
|
242
|
-
# Handle
|
|
243
|
-
|
|
555
|
+
# Handle FLM models
|
|
556
|
+
if model_info.get("recipe") == "flm":
|
|
557
|
+
try:
|
|
558
|
+
command = ["flm", "remove", checkpoint]
|
|
559
|
+
subprocess.run(command, check=True, encoding="utf-8", errors="replace")
|
|
560
|
+
print(f"Successfully deleted FLM model: {model_name}")
|
|
561
|
+
return
|
|
562
|
+
except subprocess.CalledProcessError as e:
|
|
563
|
+
raise ValueError(f"Failed to delete FLM model {model_name}: {e}") from e
|
|
564
|
+
|
|
565
|
+
if checkpoint.startswith("models--"):
|
|
566
|
+
# This is already in cache directory format (local model)
|
|
567
|
+
# Extract just the base directory name (models--{name}) from checkpoint
|
|
568
|
+
# which might contain full file path like models--name\files\model.gguf
|
|
569
|
+
checkpoint_parts = checkpoint.replace("\\", "/").split("/")
|
|
570
|
+
base_checkpoint = checkpoint_parts[0] # Just the models--{name} part
|
|
571
|
+
model_cache_dir = os.path.join(HF_HUB_CACHE, base_checkpoint)
|
|
572
|
+
|
|
573
|
+
if os.path.exists(model_cache_dir):
|
|
574
|
+
shutil.rmtree(model_cache_dir)
|
|
575
|
+
print(
|
|
576
|
+
f"Successfully deleted local model {model_name} from {model_cache_dir}"
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
print(
|
|
580
|
+
f"Model {model_name} directory not found at {model_cache_dir} - may have been manually deleted"
|
|
581
|
+
)
|
|
244
582
|
|
|
583
|
+
# Clean up user models registry
|
|
584
|
+
if model_name.startswith("user.") and os.path.exists(USER_MODELS_FILE):
|
|
585
|
+
with open(USER_MODELS_FILE, "r", encoding="utf-8") as file:
|
|
586
|
+
user_models = json.load(file)
|
|
587
|
+
|
|
588
|
+
base_model_name = model_name[5:] # Remove "user." prefix
|
|
589
|
+
if base_model_name in user_models:
|
|
590
|
+
del user_models[base_model_name]
|
|
591
|
+
with open(USER_MODELS_FILE, "w", encoding="utf-8") as file:
|
|
592
|
+
json.dump(user_models, file)
|
|
593
|
+
print(f"Removed {model_name} from user models registry")
|
|
594
|
+
|
|
595
|
+
return
|
|
596
|
+
# Parse checkpoint to get base and variant
|
|
597
|
+
base_checkpoint, variant = parse_checkpoint(checkpoint)
|
|
598
|
+
|
|
599
|
+
# Get the repository cache directory
|
|
600
|
+
snapshot_path = None
|
|
601
|
+
model_cache_dir = None
|
|
245
602
|
try:
|
|
246
|
-
#
|
|
603
|
+
# First, try to get the local path using snapshot_download with local_files_only=True
|
|
247
604
|
snapshot_path = custom_snapshot_download(
|
|
248
605
|
base_checkpoint, local_files_only=True
|
|
249
606
|
)
|
|
250
|
-
|
|
251
607
|
# Navigate up to the model directory (parent of snapshots directory)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# Delete the entire model directory (including all snapshots)
|
|
255
|
-
if os.path.exists(model_path):
|
|
256
|
-
shutil.rmtree(model_path)
|
|
257
|
-
print(f"Successfully deleted model {model_name} from {model_path}")
|
|
258
|
-
else:
|
|
259
|
-
raise ValueError(
|
|
260
|
-
f"Model {model_name} not found locally at {model_path}"
|
|
261
|
-
)
|
|
608
|
+
model_cache_dir = os.path.dirname(os.path.dirname(snapshot_path))
|
|
262
609
|
|
|
263
610
|
except Exception as e:
|
|
611
|
+
# If snapshot_download fails, try to construct the cache path manually
|
|
264
612
|
if (
|
|
265
613
|
"not found in cache" in str(e).lower()
|
|
266
|
-
or "
|
|
614
|
+
or "localentrynotfounderror" in str(e).lower()
|
|
615
|
+
or "cannot find an appropriate cached snapshot" in str(e).lower()
|
|
267
616
|
):
|
|
268
|
-
|
|
617
|
+
# Construct the Hugging Face cache path manually
|
|
618
|
+
cache_home = huggingface_hub.constants.HF_HUB_CACHE
|
|
619
|
+
# Convert repo format (e.g., "unsloth/GLM-4.5-Air-GGUF") to cache format
|
|
620
|
+
repo_cache_name = base_checkpoint.replace("/", "--")
|
|
621
|
+
model_cache_dir = os.path.join(cache_home, f"models--{repo_cache_name}")
|
|
622
|
+
# Try to find the snapshot path within the model cache directory
|
|
623
|
+
if os.path.exists(model_cache_dir):
|
|
624
|
+
snapshots_dir = os.path.join(model_cache_dir, "snapshots")
|
|
625
|
+
if os.path.exists(snapshots_dir):
|
|
626
|
+
snapshot_dirs = [
|
|
627
|
+
d
|
|
628
|
+
for d in os.listdir(snapshots_dir)
|
|
629
|
+
if os.path.isdir(os.path.join(snapshots_dir, d))
|
|
630
|
+
]
|
|
631
|
+
if snapshot_dirs:
|
|
632
|
+
# Use the first (likely only) snapshot directory
|
|
633
|
+
snapshot_path = os.path.join(
|
|
634
|
+
snapshots_dir, snapshot_dirs[0]
|
|
635
|
+
)
|
|
269
636
|
else:
|
|
270
637
|
raise ValueError(f"Failed to delete model {model_name}: {str(e)}")
|
|
271
638
|
|
|
639
|
+
# Handle deletion based on whether this is a GGUF model with variants
|
|
640
|
+
if variant and snapshot_path and os.path.exists(snapshot_path):
|
|
641
|
+
# This is a GGUF model with a specific variant - delete only variant files
|
|
642
|
+
try:
|
|
643
|
+
from lemonade.tools.llamacpp.utils import identify_gguf_models
|
|
644
|
+
|
|
645
|
+
# Get the specific files for this variant
|
|
646
|
+
core_files, sharded_files = identify_gguf_models(
|
|
647
|
+
base_checkpoint,
|
|
648
|
+
variant,
|
|
649
|
+
self.supported_models[model_name].get("mmproj", ""),
|
|
650
|
+
)
|
|
651
|
+
all_variant_files = list(core_files.values()) + sharded_files
|
|
652
|
+
|
|
653
|
+
# Delete the specific variant files
|
|
654
|
+
deleted_files = []
|
|
655
|
+
for file_path in all_variant_files:
|
|
656
|
+
full_file_path = os.path.join(snapshot_path, file_path)
|
|
657
|
+
if os.path.exists(full_file_path):
|
|
658
|
+
if os.path.isfile(full_file_path):
|
|
659
|
+
os.remove(full_file_path)
|
|
660
|
+
deleted_files.append(file_path)
|
|
661
|
+
elif os.path.isdir(full_file_path):
|
|
662
|
+
shutil.rmtree(full_file_path)
|
|
663
|
+
deleted_files.append(file_path)
|
|
664
|
+
|
|
665
|
+
if deleted_files:
|
|
666
|
+
print(f"Successfully deleted variant files: {deleted_files}")
|
|
667
|
+
else:
|
|
668
|
+
print(f"No variant files found for {variant} in {snapshot_path}")
|
|
669
|
+
|
|
670
|
+
# Check if the snapshot directory is now empty (only containing .gitattributes, README, etc.)
|
|
671
|
+
remaining_files = [
|
|
672
|
+
f
|
|
673
|
+
for f in os.listdir(snapshot_path)
|
|
674
|
+
if f.endswith(".gguf")
|
|
675
|
+
or os.path.isdir(os.path.join(snapshot_path, f))
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
# If no GGUF files remain, we can delete the entire repository
|
|
679
|
+
if not remaining_files:
|
|
680
|
+
print(f"No other variants remain, deleting entire repository cache")
|
|
681
|
+
shutil.rmtree(model_cache_dir)
|
|
682
|
+
print(
|
|
683
|
+
f"Successfully deleted entire model cache at {model_cache_dir}"
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
print(
|
|
687
|
+
f"Other variants still exist in repository, keeping cache directory"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
except Exception as variant_error:
|
|
691
|
+
print(
|
|
692
|
+
f"Warning: Could not perform selective variant deletion: {variant_error}"
|
|
693
|
+
)
|
|
694
|
+
print("This may indicate the files were already manually deleted")
|
|
695
|
+
|
|
696
|
+
elif model_cache_dir and os.path.exists(model_cache_dir):
|
|
697
|
+
# Non-GGUF model or GGUF without variant - delete entire repository as before
|
|
698
|
+
shutil.rmtree(model_cache_dir)
|
|
699
|
+
print(f"Successfully deleted model {model_name} from {model_cache_dir}")
|
|
700
|
+
|
|
701
|
+
elif model_cache_dir:
|
|
702
|
+
# Model directory doesn't exist - it was likely already manually deleted
|
|
703
|
+
print(
|
|
704
|
+
f"Model {model_name} directory not found at {model_cache_dir} - may have been manually deleted"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
else:
|
|
708
|
+
raise ValueError(f"Unable to determine cache path for model {model_name}")
|
|
709
|
+
|
|
710
|
+
# Clean up user models registry if applicable
|
|
711
|
+
if model_name.startswith("user.") and os.path.exists(USER_MODELS_FILE):
|
|
712
|
+
with open(USER_MODELS_FILE, "r", encoding="utf-8") as file:
|
|
713
|
+
user_models = json.load(file)
|
|
714
|
+
|
|
715
|
+
# Remove the "user." prefix to get the actual model name in the file
|
|
716
|
+
base_model_name = model_name[5:] # Remove "user." prefix
|
|
717
|
+
|
|
718
|
+
if base_model_name in user_models:
|
|
719
|
+
del user_models[base_model_name]
|
|
720
|
+
with open(USER_MODELS_FILE, "w", encoding="utf-8") as file:
|
|
721
|
+
json.dump(user_models, file)
|
|
722
|
+
print(f"Removed {model_name} from user models registry")
|
|
723
|
+
|
|
724
|
+
def get_incompatible_ryzenai_models(self):
|
|
725
|
+
"""
|
|
726
|
+
Get information about incompatible RyzenAI models in the cache.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
dict with 'models' list and 'total_size' info
|
|
730
|
+
"""
|
|
731
|
+
# Get HF_HOME from environment
|
|
732
|
+
hf_home = os.environ.get("HF_HOME", None)
|
|
733
|
+
|
|
734
|
+
incompatible_models, total_size = detect_incompatible_ryzenai_models(
|
|
735
|
+
DEFAULT_CACHE_DIR, hf_home
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
"models": incompatible_models,
|
|
740
|
+
"total_size": total_size,
|
|
741
|
+
"count": len(incompatible_models),
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
def cleanup_incompatible_models(self, model_paths: list):
|
|
745
|
+
"""
|
|
746
|
+
Delete incompatible RyzenAI models from the cache.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
model_paths: List of model paths to delete
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
dict with deletion results
|
|
753
|
+
"""
|
|
754
|
+
return delete_incompatible_models(model_paths)
|
|
755
|
+
|
|
272
756
|
|
|
273
757
|
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
274
758
|
# Modifications Copyright (c) 2025 AMD
|