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_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
- process.wait(timeout=10)
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
- webbrowser.open(url)
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")
@@ -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
- base_checkpoint = parse_checkpoint(
85
- self.supported_models[model]["checkpoint"]
86
- )[0]
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
- downloaded_models[model] = self.supported_models[model]
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
- # Handle GGUF models that have the format "checkpoint:variant"
243
- base_checkpoint = parse_checkpoint(checkpoint)[0]
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
- # Get the local path using snapshot_download with local_files_only=True
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
- model_path = os.path.dirname(os.path.dirname(snapshot_path))
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 "no such file" in str(e).lower()
343
+ or "localentrynotfounderror" in str(e).lower()
344
+ or "cannot find an appropriate cached snapshot" in str(e).lower()
267
345
  ):
268
- raise ValueError(f"Model {model_name} is not installed locally")
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