lemonade-sdk 8.1.5__py3-none-any.whl → 8.1.6__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/tools/llamacpp/utils.py +5 -1
- lemonade/tools/server/llamacpp.py +164 -562
- lemonade/tools/server/serve.py +15 -22
- lemonade/tools/server/wrapped_server.py +485 -0
- lemonade/version.py +1 -1
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/METADATA +1 -1
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/RECORD +14 -13
- lemonade_server/cli.py +18 -9
- lemonade_server/model_manager.py +201 -20
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.5.dist-info → lemonade_sdk-8.1.6.dist-info}/top_level.txt +0 -0
lemonade_server/cli.py
CHANGED
|
@@ -144,8 +144,12 @@ def stop():
|
|
|
144
144
|
except psutil.NoSuchProcess:
|
|
145
145
|
pass # Child already terminated
|
|
146
146
|
|
|
147
|
-
# Wait for main process
|
|
148
|
-
|
|
147
|
+
# Wait for main process to terminate gracefully
|
|
148
|
+
# kill if it doesn't terminate gracefully
|
|
149
|
+
try:
|
|
150
|
+
process.wait(timeout=5)
|
|
151
|
+
except psutil.TimeoutExpired:
|
|
152
|
+
process.kill()
|
|
149
153
|
|
|
150
154
|
# Kill llama-server child process if it didn't terminate gracefully
|
|
151
155
|
for child in children:
|
|
@@ -273,6 +277,7 @@ def run(
|
|
|
273
277
|
"""
|
|
274
278
|
import webbrowser
|
|
275
279
|
import time
|
|
280
|
+
import os
|
|
276
281
|
|
|
277
282
|
# Start the server if not running
|
|
278
283
|
_, running_port = get_server_info()
|
|
@@ -299,7 +304,10 @@ def run(
|
|
|
299
304
|
# Open the webapp with the specified model
|
|
300
305
|
url = f"http://{host}:{port}/?model={model_name}#llm-chat"
|
|
301
306
|
print(f"You can now chat with {model_name} at {url}")
|
|
302
|
-
|
|
307
|
+
|
|
308
|
+
# Only open browser if not disabled via environment variable
|
|
309
|
+
if not os.environ.get("LEMONADE_DISABLE_BROWSER"):
|
|
310
|
+
webbrowser.open(url)
|
|
303
311
|
|
|
304
312
|
# Keep the server running if we started it
|
|
305
313
|
if not server_previously_running:
|
|
@@ -507,6 +515,13 @@ def _add_server_arguments(parser):
|
|
|
507
515
|
default=DEFAULT_CTX_SIZE,
|
|
508
516
|
)
|
|
509
517
|
|
|
518
|
+
if os.name == "nt":
|
|
519
|
+
parser.add_argument(
|
|
520
|
+
"--no-tray",
|
|
521
|
+
action="store_true",
|
|
522
|
+
help="Do not show a tray icon when the server is running",
|
|
523
|
+
)
|
|
524
|
+
|
|
510
525
|
|
|
511
526
|
def main():
|
|
512
527
|
parser = argparse.ArgumentParser(
|
|
@@ -527,12 +542,6 @@ def main():
|
|
|
527
542
|
# Serve command
|
|
528
543
|
serve_parser = subparsers.add_parser("serve", help="Start server")
|
|
529
544
|
_add_server_arguments(serve_parser)
|
|
530
|
-
if os.name == "nt":
|
|
531
|
-
serve_parser.add_argument(
|
|
532
|
-
"--no-tray",
|
|
533
|
-
action="store_true",
|
|
534
|
-
help="Do not show a tray icon when the server is running",
|
|
535
|
-
)
|
|
536
545
|
|
|
537
546
|
# Status command
|
|
538
547
|
status_parser = subparsers.add_parser("status", help="Check if server is running")
|
lemonade_server/model_manager.py
CHANGED
|
@@ -77,15 +77,50 @@ class ModelManager:
|
|
|
77
77
|
def downloaded_models(self) -> dict:
|
|
78
78
|
"""
|
|
79
79
|
Returns a dictionary of locally available models.
|
|
80
|
+
For GGUF models with variants, checks if the specific variant files exist.
|
|
80
81
|
"""
|
|
81
82
|
downloaded_models = {}
|
|
82
83
|
downloaded_checkpoints = self.downloaded_hf_checkpoints
|
|
83
84
|
for model in self.supported_models:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
85
|
+
model_info = self.supported_models[model]
|
|
86
|
+
checkpoint = model_info["checkpoint"]
|
|
87
|
+
base_checkpoint, variant = parse_checkpoint(checkpoint)
|
|
88
|
+
|
|
87
89
|
if base_checkpoint in downloaded_checkpoints:
|
|
88
|
-
|
|
90
|
+
# For GGUF models with variants, verify the specific variant files exist
|
|
91
|
+
if variant and model_info.get("recipe") == "llamacpp":
|
|
92
|
+
try:
|
|
93
|
+
from lemonade.tools.llamacpp.utils import identify_gguf_models
|
|
94
|
+
from lemonade.common.network import custom_snapshot_download
|
|
95
|
+
|
|
96
|
+
# Get the local snapshot path
|
|
97
|
+
snapshot_path = custom_snapshot_download(
|
|
98
|
+
base_checkpoint, local_files_only=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Check if the specific variant files exist
|
|
102
|
+
core_files, sharded_files = identify_gguf_models(
|
|
103
|
+
base_checkpoint, variant, model_info.get("mmproj", "")
|
|
104
|
+
)
|
|
105
|
+
all_variant_files = list(core_files.values()) + sharded_files
|
|
106
|
+
|
|
107
|
+
# Verify all required files exist locally
|
|
108
|
+
all_files_exist = True
|
|
109
|
+
for file_path in all_variant_files:
|
|
110
|
+
full_file_path = os.path.join(snapshot_path, file_path)
|
|
111
|
+
if not os.path.exists(full_file_path):
|
|
112
|
+
all_files_exist = False
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if all_files_exist:
|
|
116
|
+
downloaded_models[model] = model_info
|
|
117
|
+
|
|
118
|
+
except Exception:
|
|
119
|
+
# If we can't verify the variant, don't include it
|
|
120
|
+
pass
|
|
121
|
+
else:
|
|
122
|
+
# For non-GGUF models or GGUF without variants, use the original logic
|
|
123
|
+
downloaded_models[model] = model_info
|
|
89
124
|
return downloaded_models
|
|
90
125
|
|
|
91
126
|
@property
|
|
@@ -166,6 +201,53 @@ class ModelManager:
|
|
|
166
201
|
reasoning=reasoning,
|
|
167
202
|
)
|
|
168
203
|
else:
|
|
204
|
+
# Model is already registered - check if trying to register with different parameters
|
|
205
|
+
existing_model = self.supported_models[model]
|
|
206
|
+
existing_checkpoint = existing_model.get("checkpoint")
|
|
207
|
+
existing_recipe = existing_model.get("recipe")
|
|
208
|
+
existing_reasoning = "reasoning" in existing_model.get("labels", [])
|
|
209
|
+
existing_mmproj = existing_model.get("mmproj", "")
|
|
210
|
+
|
|
211
|
+
# Compare parameters (handle None/empty string equivalence for mmproj)
|
|
212
|
+
checkpoint_differs = checkpoint and checkpoint != existing_checkpoint
|
|
213
|
+
recipe_differs = recipe and recipe != existing_recipe
|
|
214
|
+
reasoning_differs = reasoning != existing_reasoning
|
|
215
|
+
mmproj_differs = mmproj != existing_mmproj and not (
|
|
216
|
+
not mmproj and not existing_mmproj
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
checkpoint_differs
|
|
221
|
+
or recipe_differs
|
|
222
|
+
or reasoning_differs
|
|
223
|
+
or mmproj_differs
|
|
224
|
+
):
|
|
225
|
+
conflicts = []
|
|
226
|
+
if checkpoint_differs:
|
|
227
|
+
conflicts.append(
|
|
228
|
+
f"checkpoint (existing: '{existing_checkpoint}', new: '{checkpoint}')"
|
|
229
|
+
)
|
|
230
|
+
if recipe_differs:
|
|
231
|
+
conflicts.append(
|
|
232
|
+
f"recipe (existing: '{existing_recipe}', new: '{recipe}')"
|
|
233
|
+
)
|
|
234
|
+
if reasoning_differs:
|
|
235
|
+
conflicts.append(
|
|
236
|
+
f"reasoning (existing: {existing_reasoning}, new: {reasoning})"
|
|
237
|
+
)
|
|
238
|
+
if mmproj_differs:
|
|
239
|
+
conflicts.append(
|
|
240
|
+
f"mmproj (existing: '{existing_mmproj}', new: '{mmproj}')"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
conflict_details = ", ".join(conflicts)
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Model {model} is already registered with different configuration. "
|
|
246
|
+
f"Conflicting parameters: {conflict_details}. "
|
|
247
|
+
f"Please use a different model name or delete the existing model first using "
|
|
248
|
+
f"`lemonade-server delete {model}`."
|
|
249
|
+
)
|
|
250
|
+
|
|
169
251
|
new_registration_model_config = None
|
|
170
252
|
|
|
171
253
|
# Download the model
|
|
@@ -229,6 +311,7 @@ class ModelManager:
|
|
|
229
311
|
def delete_model(self, model_name: str):
|
|
230
312
|
"""
|
|
231
313
|
Deletes the specified model from local storage.
|
|
314
|
+
For GGUF models with variants, only deletes the specific variant files.
|
|
232
315
|
"""
|
|
233
316
|
if model_name not in self.supported_models:
|
|
234
317
|
raise ValueError(
|
|
@@ -239,36 +322,134 @@ class ModelManager:
|
|
|
239
322
|
checkpoint = self.supported_models[model_name]["checkpoint"]
|
|
240
323
|
print(f"Deleting {model_name} ({checkpoint})")
|
|
241
324
|
|
|
242
|
-
#
|
|
243
|
-
base_checkpoint = parse_checkpoint(checkpoint)
|
|
325
|
+
# Parse checkpoint to get base and variant
|
|
326
|
+
base_checkpoint, variant = parse_checkpoint(checkpoint)
|
|
244
327
|
|
|
328
|
+
# Get the repository cache directory
|
|
329
|
+
snapshot_path = None
|
|
330
|
+
model_cache_dir = None
|
|
245
331
|
try:
|
|
246
|
-
#
|
|
332
|
+
# First, try to get the local path using snapshot_download with local_files_only=True
|
|
247
333
|
snapshot_path = custom_snapshot_download(
|
|
248
334
|
base_checkpoint, local_files_only=True
|
|
249
335
|
)
|
|
250
|
-
|
|
251
336
|
# 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
|
-
)
|
|
337
|
+
model_cache_dir = os.path.dirname(os.path.dirname(snapshot_path))
|
|
262
338
|
|
|
263
339
|
except Exception as e:
|
|
340
|
+
# If snapshot_download fails, try to construct the cache path manually
|
|
264
341
|
if (
|
|
265
342
|
"not found in cache" in str(e).lower()
|
|
266
|
-
or "
|
|
343
|
+
or "localentrynotfounderror" in str(e).lower()
|
|
344
|
+
or "cannot find an appropriate cached snapshot" in str(e).lower()
|
|
267
345
|
):
|
|
268
|
-
|
|
346
|
+
# Construct the Hugging Face cache path manually
|
|
347
|
+
cache_home = huggingface_hub.constants.HF_HUB_CACHE
|
|
348
|
+
# Convert repo format (e.g., "unsloth/GLM-4.5-Air-GGUF") to cache format
|
|
349
|
+
repo_cache_name = base_checkpoint.replace("/", "--")
|
|
350
|
+
model_cache_dir = os.path.join(cache_home, f"models--{repo_cache_name}")
|
|
351
|
+
# Try to find the snapshot path within the model cache directory
|
|
352
|
+
if os.path.exists(model_cache_dir):
|
|
353
|
+
snapshots_dir = os.path.join(model_cache_dir, "snapshots")
|
|
354
|
+
if os.path.exists(snapshots_dir):
|
|
355
|
+
snapshot_dirs = [
|
|
356
|
+
d
|
|
357
|
+
for d in os.listdir(snapshots_dir)
|
|
358
|
+
if os.path.isdir(os.path.join(snapshots_dir, d))
|
|
359
|
+
]
|
|
360
|
+
if snapshot_dirs:
|
|
361
|
+
# Use the first (likely only) snapshot directory
|
|
362
|
+
snapshot_path = os.path.join(
|
|
363
|
+
snapshots_dir, snapshot_dirs[0]
|
|
364
|
+
)
|
|
269
365
|
else:
|
|
270
366
|
raise ValueError(f"Failed to delete model {model_name}: {str(e)}")
|
|
271
367
|
|
|
368
|
+
# Handle deletion based on whether this is a GGUF model with variants
|
|
369
|
+
if variant and snapshot_path and os.path.exists(snapshot_path):
|
|
370
|
+
# This is a GGUF model with a specific variant - delete only variant files
|
|
371
|
+
try:
|
|
372
|
+
from lemonade.tools.llamacpp.utils import identify_gguf_models
|
|
373
|
+
|
|
374
|
+
# Get the specific files for this variant
|
|
375
|
+
core_files, sharded_files = identify_gguf_models(
|
|
376
|
+
base_checkpoint,
|
|
377
|
+
variant,
|
|
378
|
+
self.supported_models[model_name].get("mmproj", ""),
|
|
379
|
+
)
|
|
380
|
+
all_variant_files = list(core_files.values()) + sharded_files
|
|
381
|
+
|
|
382
|
+
# Delete the specific variant files
|
|
383
|
+
deleted_files = []
|
|
384
|
+
for file_path in all_variant_files:
|
|
385
|
+
full_file_path = os.path.join(snapshot_path, file_path)
|
|
386
|
+
if os.path.exists(full_file_path):
|
|
387
|
+
if os.path.isfile(full_file_path):
|
|
388
|
+
os.remove(full_file_path)
|
|
389
|
+
deleted_files.append(file_path)
|
|
390
|
+
elif os.path.isdir(full_file_path):
|
|
391
|
+
shutil.rmtree(full_file_path)
|
|
392
|
+
deleted_files.append(file_path)
|
|
393
|
+
|
|
394
|
+
if deleted_files:
|
|
395
|
+
print(f"Successfully deleted variant files: {deleted_files}")
|
|
396
|
+
else:
|
|
397
|
+
print(f"No variant files found for {variant} in {snapshot_path}")
|
|
398
|
+
|
|
399
|
+
# Check if the snapshot directory is now empty (only containing .gitattributes, README, etc.)
|
|
400
|
+
remaining_files = [
|
|
401
|
+
f
|
|
402
|
+
for f in os.listdir(snapshot_path)
|
|
403
|
+
if f.endswith(".gguf")
|
|
404
|
+
or os.path.isdir(os.path.join(snapshot_path, f))
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
# If no GGUF files remain, we can delete the entire repository
|
|
408
|
+
if not remaining_files:
|
|
409
|
+
print(f"No other variants remain, deleting entire repository cache")
|
|
410
|
+
shutil.rmtree(model_cache_dir)
|
|
411
|
+
print(
|
|
412
|
+
f"Successfully deleted entire model cache at {model_cache_dir}"
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
print(
|
|
416
|
+
f"Other variants still exist in repository, keeping cache directory"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
except Exception as variant_error:
|
|
420
|
+
print(
|
|
421
|
+
f"Warning: Could not perform selective variant deletion: {variant_error}"
|
|
422
|
+
)
|
|
423
|
+
print("This may indicate the files were already manually deleted")
|
|
424
|
+
|
|
425
|
+
elif model_cache_dir and os.path.exists(model_cache_dir):
|
|
426
|
+
# Non-GGUF model or GGUF without variant - delete entire repository as before
|
|
427
|
+
shutil.rmtree(model_cache_dir)
|
|
428
|
+
print(f"Successfully deleted model {model_name} from {model_cache_dir}")
|
|
429
|
+
|
|
430
|
+
elif model_cache_dir:
|
|
431
|
+
# Model directory doesn't exist - it was likely already manually deleted
|
|
432
|
+
print(
|
|
433
|
+
f"Model {model_name} directory not found at {model_cache_dir} - may have been manually deleted"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
else:
|
|
437
|
+
raise ValueError(f"Unable to determine cache path for model {model_name}")
|
|
438
|
+
|
|
439
|
+
# Clean up user models registry if applicable
|
|
440
|
+
if model_name.startswith("user.") and os.path.exists(USER_MODELS_FILE):
|
|
441
|
+
with open(USER_MODELS_FILE, "r", encoding="utf-8") as file:
|
|
442
|
+
user_models = json.load(file)
|
|
443
|
+
|
|
444
|
+
# Remove the "user." prefix to get the actual model name in the file
|
|
445
|
+
base_model_name = model_name[5:] # Remove "user." prefix
|
|
446
|
+
|
|
447
|
+
if base_model_name in user_models:
|
|
448
|
+
del user_models[base_model_name]
|
|
449
|
+
with open(USER_MODELS_FILE, "w", encoding="utf-8") as file:
|
|
450
|
+
json.dump(user_models, file)
|
|
451
|
+
print(f"Removed {model_name} from user models registry")
|
|
452
|
+
|
|
272
453
|
|
|
273
454
|
# This file was originally licensed under Apache 2.0. It has been modified.
|
|
274
455
|
# Modifications Copyright (c) 2025 AMD
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|