lemonade-sdk 8.1.3__py3-none-any.whl → 8.1.5__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/api.py +5 -0
- lemonade/common/network.py +27 -1
- lemonade/tools/llamacpp/utils.py +2 -1
- lemonade/tools/oga/load.py +2 -1
- lemonade/tools/server/llamacpp.py +8 -2
- lemonade/tools/server/serve.py +43 -1
- lemonade/tools/server/static/styles.css +2 -2
- lemonade/tools/server/tray.py +51 -3
- lemonade/version.py +1 -1
- lemonade_install/install.py +1 -1
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/METADATA +20 -47
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/RECORD +21 -20
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/licenses/NOTICE.md +27 -1
- lemonade_server/cli.py +6 -7
- lemonade_server/model_manager.py +12 -2
- lemonade_server/pydantic_models.py +8 -0
- lemonade_server/settings.py +39 -0
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.3.dist-info → lemonade_sdk-8.1.5.dist-info}/top_level.txt +0 -0
lemonade/api.py
CHANGED
|
@@ -36,6 +36,7 @@ def _make_state(recipe, checkpoint) -> Dict:
|
|
|
36
36
|
def from_pretrained(
|
|
37
37
|
checkpoint: str,
|
|
38
38
|
recipe: str = "hf-cpu",
|
|
39
|
+
do_not_upgrade: bool = True,
|
|
39
40
|
) -> Tuple[ModelAdapter, TokenizerAdapter]:
|
|
40
41
|
"""
|
|
41
42
|
Load an LLM and the corresponding tokenizer using a lemonade recipe.
|
|
@@ -43,6 +44,9 @@ def from_pretrained(
|
|
|
43
44
|
Args:
|
|
44
45
|
- checkpoint: huggingface checkpoint that defines the LLM
|
|
45
46
|
- recipe: defines the implementation and hardware used for the LLM
|
|
47
|
+
- do_not_upgrade: prioritize the local copy of the model, if available,
|
|
48
|
+
even if an upgraded copy is available on the server (note: only applies
|
|
49
|
+
for oga-* recipes)
|
|
46
50
|
|
|
47
51
|
Recipe choices:
|
|
48
52
|
- hf-cpu: Huggingface Transformers implementation for CPU with max-perf settings
|
|
@@ -118,6 +122,7 @@ def from_pretrained(
|
|
|
118
122
|
input=checkpoint,
|
|
119
123
|
device=user_backend,
|
|
120
124
|
dtype=backend_to_dtype[user_backend],
|
|
125
|
+
do_not_upgrade=do_not_upgrade,
|
|
121
126
|
)
|
|
122
127
|
|
|
123
128
|
return state.model, state.tokenizer
|
lemonade/common/network.py
CHANGED
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
import socket
|
|
4
4
|
from huggingface_hub import model_info, snapshot_download
|
|
5
|
+
from huggingface_hub.errors import LocalEntryNotFoundError
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def is_offline():
|
|
@@ -50,10 +51,11 @@ def get_base_model(checkpoint: str) -> Optional[str]:
|
|
|
50
51
|
return None
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
def
|
|
54
|
+
def _symlink_safe_snapshot_download(repo_id, **kwargs):
|
|
54
55
|
"""
|
|
55
56
|
Custom snapshot download with retry logic for Windows symlink privilege errors.
|
|
56
57
|
"""
|
|
58
|
+
|
|
57
59
|
for attempt in range(2):
|
|
58
60
|
try:
|
|
59
61
|
return snapshot_download(repo_id=repo_id, **kwargs)
|
|
@@ -65,3 +67,27 @@ def custom_snapshot_download(repo_id, **kwargs):
|
|
|
65
67
|
):
|
|
66
68
|
continue
|
|
67
69
|
raise
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def custom_snapshot_download(repo_id, do_not_upgrade=False, **kwargs):
|
|
73
|
+
"""
|
|
74
|
+
Custom snapshot download with:
|
|
75
|
+
1) retry logic for Windows symlink privilege errors.
|
|
76
|
+
2) do_not_upgrade allows the caller to prioritize a local copy
|
|
77
|
+
of the model over an upgraded remote copy.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
if do_not_upgrade:
|
|
81
|
+
try:
|
|
82
|
+
# Prioritize the local model, if available
|
|
83
|
+
return _symlink_safe_snapshot_download(
|
|
84
|
+
repo_id, local_files_only=True, **kwargs
|
|
85
|
+
)
|
|
86
|
+
except LocalEntryNotFoundError:
|
|
87
|
+
# LocalEntryNotFoundError means there was no local model, at this point
|
|
88
|
+
# we'll accept a remote model
|
|
89
|
+
return _symlink_safe_snapshot_download(
|
|
90
|
+
repo_id, local_files_only=False, **kwargs
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
return _symlink_safe_snapshot_download(repo_id, **kwargs)
|
lemonade/tools/llamacpp/utils.py
CHANGED
|
@@ -585,7 +585,7 @@ def identify_gguf_models(
|
|
|
585
585
|
return core_files, sharded_files
|
|
586
586
|
|
|
587
587
|
|
|
588
|
-
def download_gguf(config_checkpoint, config_mmproj=None) -> dict:
|
|
588
|
+
def download_gguf(config_checkpoint, config_mmproj=None, do_not_upgrade=False) -> dict:
|
|
589
589
|
"""
|
|
590
590
|
Downloads the GGUF file for the given model configuration.
|
|
591
591
|
|
|
@@ -605,6 +605,7 @@ def download_gguf(config_checkpoint, config_mmproj=None) -> dict:
|
|
|
605
605
|
snapshot_folder = custom_snapshot_download(
|
|
606
606
|
checkpoint,
|
|
607
607
|
allow_patterns=list(core_files.values()) + sharded_files,
|
|
608
|
+
do_not_upgrade=do_not_upgrade,
|
|
608
609
|
)
|
|
609
610
|
|
|
610
611
|
# Ensure we downloaded all expected files
|
lemonade/tools/oga/load.py
CHANGED
|
@@ -654,6 +654,7 @@ class OgaLoad(FirstTool):
|
|
|
654
654
|
download_only: bool = False,
|
|
655
655
|
trust_remote_code=False,
|
|
656
656
|
subfolder: str = None,
|
|
657
|
+
do_not_upgrade: bool = False,
|
|
657
658
|
) -> State:
|
|
658
659
|
from lemonade.common.network import (
|
|
659
660
|
custom_snapshot_download,
|
|
@@ -744,7 +745,7 @@ class OgaLoad(FirstTool):
|
|
|
744
745
|
input_model_path = custom_snapshot_download(
|
|
745
746
|
checkpoint,
|
|
746
747
|
ignore_patterns=["*.md", "*.txt"],
|
|
747
|
-
local_files_only=offline,
|
|
748
|
+
local_files_only=offline or do_not_upgrade,
|
|
748
749
|
)
|
|
749
750
|
# Check if model is ONNX or safetensors
|
|
750
751
|
is_onnx_model = any(
|
|
@@ -378,7 +378,11 @@ def _launch_llama_subprocess(
|
|
|
378
378
|
|
|
379
379
|
|
|
380
380
|
def server_load(
|
|
381
|
-
model_config: PullConfig,
|
|
381
|
+
model_config: PullConfig,
|
|
382
|
+
telemetry: LlamaTelemetry,
|
|
383
|
+
backend: str,
|
|
384
|
+
ctx_size: int,
|
|
385
|
+
do_not_upgrade: bool = False,
|
|
382
386
|
):
|
|
383
387
|
# Install and/or update llama.cpp if needed
|
|
384
388
|
try:
|
|
@@ -389,7 +393,9 @@ def server_load(
|
|
|
389
393
|
)
|
|
390
394
|
|
|
391
395
|
# Download the gguf to the hugging face cache
|
|
392
|
-
snapshot_files = download_gguf(
|
|
396
|
+
snapshot_files = download_gguf(
|
|
397
|
+
model_config.checkpoint, model_config.mmproj, do_not_upgrade=do_not_upgrade
|
|
398
|
+
)
|
|
393
399
|
logging.debug(f"GGUF file paths: {snapshot_files}")
|
|
394
400
|
|
|
395
401
|
# Check if model supports embeddings
|
lemonade/tools/server/serve.py
CHANGED
|
@@ -67,7 +67,9 @@ from lemonade_server.pydantic_models import (
|
|
|
67
67
|
ResponsesRequest,
|
|
68
68
|
PullConfig,
|
|
69
69
|
DeleteConfig,
|
|
70
|
+
LogLevelConfig,
|
|
70
71
|
)
|
|
72
|
+
from lemonade_server.settings import save_setting
|
|
71
73
|
|
|
72
74
|
# Set to a high number to allow for interesting experiences in real apps
|
|
73
75
|
# Tests should use the max_new_tokens argument to set a lower value
|
|
@@ -249,6 +251,7 @@ class Server:
|
|
|
249
251
|
self.app.get(f"{prefix}/system-info")(self.get_system_info)
|
|
250
252
|
self.app.post(f"{prefix}/completions")(self.completions)
|
|
251
253
|
self.app.post(f"{prefix}/responses")(self.responses)
|
|
254
|
+
self.app.post(f"{prefix}/log-level")(self.set_log_level)
|
|
252
255
|
|
|
253
256
|
# OpenAI-compatible routes
|
|
254
257
|
self.app.post(f"{prefix}/chat/completions")(self.chat_completions)
|
|
@@ -259,6 +262,38 @@ class Server:
|
|
|
259
262
|
self.app.post(f"{prefix}/reranking")(self.reranking)
|
|
260
263
|
self.app.post(f"{prefix}/rerank")(self.reranking)
|
|
261
264
|
|
|
265
|
+
async def set_log_level(self, config: LogLevelConfig):
|
|
266
|
+
"""
|
|
267
|
+
Set the logging level of the server.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
log_level_upper = config.level.upper()
|
|
271
|
+
numeric_level = getattr(logging, log_level_upper, None)
|
|
272
|
+
if not isinstance(numeric_level, int):
|
|
273
|
+
raise ValueError(f"Invalid log level: {config.level}")
|
|
274
|
+
|
|
275
|
+
# Get the root logger
|
|
276
|
+
logger = logging.getLogger()
|
|
277
|
+
logger.setLevel(numeric_level)
|
|
278
|
+
|
|
279
|
+
# Update all handlers
|
|
280
|
+
for handler in logger.handlers:
|
|
281
|
+
handler.setLevel(numeric_level)
|
|
282
|
+
|
|
283
|
+
logging.getLogger("uvicorn.error").setLevel(numeric_level)
|
|
284
|
+
self.debug_logging_enabled = numeric_level <= logging.DEBUG
|
|
285
|
+
|
|
286
|
+
# Save the setting
|
|
287
|
+
save_setting("log_level", config.level)
|
|
288
|
+
|
|
289
|
+
logging.info(f"Log level changed to: {config.level}")
|
|
290
|
+
return {"status": "success", "message": f"Log level set to {config.level}"}
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise HTTPException(
|
|
293
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
294
|
+
detail=f"Failed to set log level: {str(e)}",
|
|
295
|
+
)
|
|
296
|
+
|
|
262
297
|
def _log_request_parameters(self, request, endpoint_name: str):
|
|
263
298
|
"""
|
|
264
299
|
Log request parameters excluding content fields like messages, prompt, or input.
|
|
@@ -367,7 +402,9 @@ class Server:
|
|
|
367
402
|
|
|
368
403
|
# Open lemonade server in tray mode
|
|
369
404
|
# lambda function used for deferred instantiation and thread safety
|
|
370
|
-
LemonadeTray(
|
|
405
|
+
LemonadeTray(
|
|
406
|
+
self.log_file, self.port, lambda: self, log_level=self.log_level
|
|
407
|
+
).run()
|
|
371
408
|
sys.exit(0)
|
|
372
409
|
|
|
373
410
|
if self.debug_logging_enabled:
|
|
@@ -1351,6 +1388,9 @@ class Server:
|
|
|
1351
1388
|
recipe=config.recipe,
|
|
1352
1389
|
reasoning=config.reasoning,
|
|
1353
1390
|
mmproj=config.mmproj,
|
|
1391
|
+
# The pull endpoint will download an upgraded model if available, even
|
|
1392
|
+
# if we already have a local copy of the model
|
|
1393
|
+
do_not_upgrade=False,
|
|
1354
1394
|
)
|
|
1355
1395
|
|
|
1356
1396
|
# Refresh the list of downloaded models, to ensure it
|
|
@@ -1449,6 +1489,8 @@ class Server:
|
|
|
1449
1489
|
telemetry=self.llama_telemetry,
|
|
1450
1490
|
backend=self.llamacpp_backend,
|
|
1451
1491
|
ctx_size=self.ctx_size,
|
|
1492
|
+
# Models should only upgrade when using the pull endpoint
|
|
1493
|
+
do_not_upgrade=True,
|
|
1452
1494
|
)
|
|
1453
1495
|
|
|
1454
1496
|
else:
|
|
@@ -1836,7 +1836,7 @@ body::before {
|
|
|
1836
1836
|
background: #fff;
|
|
1837
1837
|
border-radius: 8px;
|
|
1838
1838
|
border: 1px solid #e0e0e0;
|
|
1839
|
-
overflow:
|
|
1839
|
+
overflow: visible;
|
|
1840
1840
|
}
|
|
1841
1841
|
|
|
1842
1842
|
.model-browser-sidebar {
|
|
@@ -1974,7 +1974,7 @@ body::before {
|
|
|
1974
1974
|
flex: 1;
|
|
1975
1975
|
display: flex;
|
|
1976
1976
|
flex-direction: column;
|
|
1977
|
-
overflow:
|
|
1977
|
+
overflow: visible;
|
|
1978
1978
|
}
|
|
1979
1979
|
|
|
1980
1980
|
.model-browser-header {
|
lemonade/tools/server/tray.py
CHANGED
|
@@ -43,7 +43,7 @@ class LemonadeTray(SystemTray):
|
|
|
43
43
|
Lemonade-specific system tray implementation.
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
-
def __init__(self, log_file, port, server_factory):
|
|
46
|
+
def __init__(self, log_file, port, server_factory, log_level="info"):
|
|
47
47
|
# Find the icon path
|
|
48
48
|
icon_path = Path(__file__).resolve().parents[0] / "static" / "favicon.ico"
|
|
49
49
|
|
|
@@ -58,6 +58,7 @@ class LemonadeTray(SystemTray):
|
|
|
58
58
|
self.log_file = log_file
|
|
59
59
|
self.port = port
|
|
60
60
|
self.server_factory = server_factory
|
|
61
|
+
self.debug_logs_enabled = log_level == "debug"
|
|
61
62
|
|
|
62
63
|
# Get current and latest version
|
|
63
64
|
self.current_version = __version__
|
|
@@ -281,11 +282,27 @@ class LemonadeTray(SystemTray):
|
|
|
281
282
|
self.logger.error(f"Error changing port: {str(e)}")
|
|
282
283
|
self.show_balloon_notification("Error", f"Failed to change port: {str(e)}")
|
|
283
284
|
|
|
285
|
+
def _using_installer(self):
|
|
286
|
+
"""
|
|
287
|
+
Check if the user is using the NSIS installer by checking for embeddable python
|
|
288
|
+
"""
|
|
289
|
+
py_home = Path(sys.executable).parent
|
|
290
|
+
pth_file = (
|
|
291
|
+
py_home / f"python{sys.version_info.major}{sys.version_info.minor}._pth"
|
|
292
|
+
)
|
|
293
|
+
return pth_file.exists()
|
|
294
|
+
|
|
284
295
|
def upgrade_to_latest(self, _, __):
|
|
285
296
|
"""
|
|
286
|
-
Download and launch the Lemonade Server installer
|
|
297
|
+
Download and launch the Lemonade Server installer if the user is using the NSIS installer
|
|
298
|
+
Otherwise, simply open the browser to the release page
|
|
287
299
|
"""
|
|
288
300
|
|
|
301
|
+
# If the user installed from source, simple open their browser to the release page
|
|
302
|
+
if not self._using_installer():
|
|
303
|
+
webbrowser.open("https://github.com/lemonade-sdk/lemonade/releases/latest")
|
|
304
|
+
return
|
|
305
|
+
|
|
289
306
|
# Show notification that download is starting
|
|
290
307
|
self.show_balloon_notification(
|
|
291
308
|
"Upgrading Lemonade",
|
|
@@ -325,6 +342,26 @@ class LemonadeTray(SystemTray):
|
|
|
325
342
|
|
|
326
343
|
# No need to quit the application, the installer will handle it
|
|
327
344
|
|
|
345
|
+
def toggle_debug_logs(self, _, __):
|
|
346
|
+
"""
|
|
347
|
+
Toggle debug logs on and off.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
new_level = "debug" if not self.debug_logs_enabled else "info"
|
|
351
|
+
response = requests.post(
|
|
352
|
+
f"http://localhost:{self.port}/api/v1/log-level",
|
|
353
|
+
json={"level": new_level},
|
|
354
|
+
)
|
|
355
|
+
response.raise_for_status()
|
|
356
|
+
self.debug_logs_enabled = not self.debug_logs_enabled
|
|
357
|
+
self.show_balloon_notification(
|
|
358
|
+
"Debug Logs",
|
|
359
|
+
f"Debug logs {'enabled' if self.debug_logs_enabled else 'disabled'}",
|
|
360
|
+
)
|
|
361
|
+
except (FileNotFoundError, ValueError) as e:
|
|
362
|
+
self.logger.error(f"Error toggling debug logs: {str(e)}")
|
|
363
|
+
self.show_balloon_notification("Error", "Failed to toggle debug logs.")
|
|
364
|
+
|
|
328
365
|
def create_menu(self):
|
|
329
366
|
"""
|
|
330
367
|
Create the Lemonade-specific context menu.
|
|
@@ -401,6 +438,17 @@ class LemonadeTray(SystemTray):
|
|
|
401
438
|
|
|
402
439
|
port_submenu = Menu(*port_menu_items)
|
|
403
440
|
|
|
441
|
+
# Create the Logs submenu
|
|
442
|
+
debug_log_text = "Enable Debug Logs"
|
|
443
|
+
debug_log_item = MenuItem(debug_log_text, self.toggle_debug_logs)
|
|
444
|
+
debug_log_item.checked = self.debug_logs_enabled
|
|
445
|
+
|
|
446
|
+
logs_submenu = Menu(
|
|
447
|
+
MenuItem("Show Logs", self.show_logs),
|
|
448
|
+
Menu.SEPARATOR,
|
|
449
|
+
debug_log_item,
|
|
450
|
+
)
|
|
451
|
+
|
|
404
452
|
if status_successfully_checked:
|
|
405
453
|
items.append(MenuItem("Load Model", None, submenu=load_submenu))
|
|
406
454
|
items.append(MenuItem("Port", None, submenu=port_submenu))
|
|
@@ -417,7 +465,7 @@ class LemonadeTray(SystemTray):
|
|
|
417
465
|
items.append(MenuItem("Documentation", self.open_documentation))
|
|
418
466
|
items.append(MenuItem("LLM Chat", self.open_llm_chat))
|
|
419
467
|
items.append(MenuItem("Model Manager", self.open_model_manager))
|
|
420
|
-
items.append(MenuItem("
|
|
468
|
+
items.append(MenuItem("Logs", None, submenu=logs_submenu))
|
|
421
469
|
items.append(Menu.SEPARATOR)
|
|
422
470
|
items.append(MenuItem("Quit Lemonade", self.exit_app))
|
|
423
471
|
return Menu(*items)
|
lemonade/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "8.1.
|
|
1
|
+
__version__ = "8.1.5"
|
lemonade_install/install.py
CHANGED
|
@@ -162,7 +162,7 @@ def _get_ryzenai_version_info(device=None):
|
|
|
162
162
|
|
|
163
163
|
if Version(og.__version__) >= Version("0.7.0"):
|
|
164
164
|
oga_path = os.path.dirname(og.__file__)
|
|
165
|
-
if og.__version__
|
|
165
|
+
if og.__version__ in ("0.7.0.2.1", "0.7.0.2"):
|
|
166
166
|
return "1.5.0", oga_path
|
|
167
167
|
else:
|
|
168
168
|
return "1.4.0", oga_path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lemonade-sdk
|
|
3
|
-
Version: 8.1.
|
|
3
|
+
Version: 8.1.5
|
|
4
4
|
Summary: Lemonade SDK: Your LLM Aide for Validation and Deployment
|
|
5
5
|
Author-email: lemonade@amd.com
|
|
6
6
|
Requires-Python: >=3.10, <3.14
|
|
@@ -30,7 +30,7 @@ Requires-Dist: sentencepiece
|
|
|
30
30
|
Requires-Dist: huggingface-hub[hf_xet]==0.33.0
|
|
31
31
|
Requires-Dist: python-dotenv
|
|
32
32
|
Provides-Extra: oga-ryzenai
|
|
33
|
-
Requires-Dist: onnxruntime-genai-directml-ryzenai==0.7.0.2; extra == "oga-ryzenai"
|
|
33
|
+
Requires-Dist: onnxruntime-genai-directml-ryzenai==0.7.0.2.1; extra == "oga-ryzenai"
|
|
34
34
|
Requires-Dist: protobuf>=6.30.1; extra == "oga-ryzenai"
|
|
35
35
|
Provides-Extra: oga-cpu
|
|
36
36
|
Requires-Dist: onnxruntime-genai==0.8.2; extra == "oga-cpu"
|
|
@@ -200,48 +200,11 @@ You can also import custom GGUF and ONNX models from Hugging Face by using our [
|
|
|
200
200
|
|
|
201
201
|
Lemonade supports the following configurations, while also making it easy to switch between them at runtime. Find more information about it [here](./docs/README.md#software-and-hardware-overview).
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<th colspan="2" align="center">🖥️ OS (x86/x64)</th>
|
|
209
|
-
</tr>
|
|
210
|
-
<tr>
|
|
211
|
-
<th align="center">OGA</th>
|
|
212
|
-
<th align="center">llamacpp</th>
|
|
213
|
-
<th align="center">HF</th>
|
|
214
|
-
<th align="center">Windows</th>
|
|
215
|
-
<th align="center">Linux</th>
|
|
216
|
-
</tr>
|
|
217
|
-
</thead>
|
|
218
|
-
<tbody>
|
|
219
|
-
<tr>
|
|
220
|
-
<td><strong>🧠 CPU</strong></td>
|
|
221
|
-
<td align="center">All platforms</td>
|
|
222
|
-
<td align="center">All platforms</td>
|
|
223
|
-
<td align="center">All platforms</td>
|
|
224
|
-
<td align="center">✅</td>
|
|
225
|
-
<td align="center">✅</td>
|
|
226
|
-
</tr>
|
|
227
|
-
<tr>
|
|
228
|
-
<td><strong>🎮 GPU</strong></td>
|
|
229
|
-
<td align="center">—</td>
|
|
230
|
-
<td align="center">Vulkan: All platforms<br>ROCm: Selected AMD platforms*</td>
|
|
231
|
-
<td align="center">—</td>
|
|
232
|
-
<td align="center">✅</td>
|
|
233
|
-
<td align="center">✅</td>
|
|
234
|
-
</tr>
|
|
235
|
-
<tr>
|
|
236
|
-
<td><strong>🤖 NPU</strong></td>
|
|
237
|
-
<td align="center">AMD Ryzen™ AI 300 series</td>
|
|
238
|
-
<td align="center">—</td>
|
|
239
|
-
<td align="center">—</td>
|
|
240
|
-
<td align="center">✅</td>
|
|
241
|
-
<td align="center">—</td>
|
|
242
|
-
</tr>
|
|
243
|
-
</tbody>
|
|
244
|
-
</table>
|
|
203
|
+
| Hardware | Engine: OGA | Engine: llamacpp | Engine: HF | Windows | Linux |
|
|
204
|
+
|----------|-------------|------------------|------------|---------|-------|
|
|
205
|
+
| **🧠 CPU** | All platforms | All platforms | All platforms | ✅ | ✅ |
|
|
206
|
+
| **🎮 GPU** | — | Vulkan: All platforms<br>ROCm: Selected AMD platforms* | — | ✅ | ✅ |
|
|
207
|
+
| **🤖 NPU** | AMD Ryzen™ AI 300 series | — | — | ✅ | — |
|
|
245
208
|
|
|
246
209
|
<details>
|
|
247
210
|
<summary><small><i>* See supported AMD ROCm platforms</i></small></summary>
|
|
@@ -337,9 +300,19 @@ New contributors can find beginner-friendly issues tagged with "Good First Issue
|
|
|
337
300
|
|
|
338
301
|
This project is sponsored by AMD. It is maintained by @danielholanda @jeremyfowers @ramkrishna @vgodsoe in equal measure. You can reach us by filing an [issue](https://github.com/lemonade-sdk/lemonade/issues), emailing [lemonade@amd.com](mailto:lemonade@amd.com), or joining our [Discord](https://discord.gg/5xXzkMu8Zk).
|
|
339
302
|
|
|
340
|
-
## License
|
|
341
|
-
|
|
342
|
-
This project is
|
|
303
|
+
## License and Attribution
|
|
304
|
+
|
|
305
|
+
This project is:
|
|
306
|
+
- [Built with Python](https://www.amd.com/en/developer/resources/technical-articles/2025/rethinking-local-ai-lemonade-servers-python-advantage.html) with ❤️ for the open source community,
|
|
307
|
+
- Standing on the shoulders of great tools from:
|
|
308
|
+
- [ggml/llama.cpp](https://github.com/ggml-org/llama.cpp)
|
|
309
|
+
- [OnnxRuntime GenAI](https://github.com/microsoft/onnxruntime-genai)
|
|
310
|
+
- [Hugging Face Hub](https://github.com/huggingface/huggingface_hub)
|
|
311
|
+
- [OpenAI API](https://github.com/openai/openai-python)
|
|
312
|
+
- and more...
|
|
313
|
+
- Accelerated by mentorship from the OCV Catalyst program.
|
|
314
|
+
- Licensed under the [Apache 2.0 License](https://github.com/lemonade-sdk/lemonade/blob/main/LICENSE).
|
|
315
|
+
- Portions of the project are licensed as described in [NOTICE.md](./NOTICE.md).
|
|
343
316
|
|
|
344
317
|
<!--This file was originally licensed under Apache 2.0. It has been modified.
|
|
345
318
|
Modifications Copyright (c) 2025 AMD-->
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
lemonade/__init__.py,sha256=W1Qk7r0rnQqFhPNHp6BIBT_q-OH3s-8Q_POoVfAmKW0,117
|
|
2
|
-
lemonade/api.py,sha256=
|
|
2
|
+
lemonade/api.py,sha256=Oc4yBA3LZg8FrTsbuDq1p9-XE74pqNnIEUhXyKa7qg8,5786
|
|
3
3
|
lemonade/cache.py,sha256=5iZbk273TiTMqK_vdzPOPYTo6VsWW2gNByOISA9zi1w,3002
|
|
4
4
|
lemonade/cli.py,sha256=9Pcs3PcrWC2F8_pcBaz09xHUICIJTvpemBdPGyXkjIk,4395
|
|
5
5
|
lemonade/sequence.py,sha256=KSH7BPsiyDKsOsg_ziQKEGsDwMmuO_YbgPRBxkZd0pw,13267
|
|
6
6
|
lemonade/state.py,sha256=sdSezla7Cd7KYL90xY3p9kcNV4ndSyN6UvNLOr3vBMA,5261
|
|
7
|
-
lemonade/version.py,sha256=
|
|
7
|
+
lemonade/version.py,sha256=UE2byv3TrjFSpmrlMMLljjtWoisXIAZ0MoNAF9Lc36k,22
|
|
8
8
|
lemonade/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
lemonade/common/build.py,sha256=zTb0m1-kuUx6zw5QHp2SNnVuN6jOTMQ2FCdj9iH374U,6140
|
|
10
10
|
lemonade/common/cli_helpers.py,sha256=hjBfXrTtFl8gmCFlL-ksviXR0mOcdPtTWVNKoEp3PG4,4993
|
|
11
11
|
lemonade/common/exceptions.py,sha256=w83sVKmL1QXoJlGjj_bRyjIBMhlMqdVQy_FEOTu2YQI,2050
|
|
12
12
|
lemonade/common/filesystem.py,sha256=QV3cHhKNu-7W2rr8wZ4JQfD2rP_5T2Js7jiDQBYWHVQ,12142
|
|
13
13
|
lemonade/common/inference_engines.py,sha256=pJxn0zOf3gEmjGAIWXNdCibfzarzc7LRbZjoQyygkcU,12591
|
|
14
|
-
lemonade/common/network.py,sha256=
|
|
14
|
+
lemonade/common/network.py,sha256=qXpUjDYQEYM_gH3JwTtU-pu_yCKcaa1IeohJRPy91-A,2903
|
|
15
15
|
lemonade/common/printing.py,sha256=GFFzrXIineIOMa9yu0lo5sL4j6A5BBg_T9aUCdP-juw,3229
|
|
16
16
|
lemonade/common/status.py,sha256=xSOZN508cdRtrs1HVyr9zmASYg69EsZBLSs0lroLoCM,16519
|
|
17
17
|
lemonade/common/system_info.py,sha256=pn-k3zMQCbt5cu3aHXa4cENgrubOK97gs9PYdGPsFXA,28405
|
|
@@ -34,10 +34,10 @@ lemonade/tools/huggingface/load.py,sha256=KsSGOBBD-tNEIfYC8mCWV_jpnkjHMhN3juVmC1
|
|
|
34
34
|
lemonade/tools/huggingface/utils.py,sha256=j1S-IgjDsznUIVwkHSqqChmFyqIx9f3WcEelzohWwvU,13955
|
|
35
35
|
lemonade/tools/llamacpp/bench.py,sha256=1fkE02ecg-jRk92i5dTAXz6re14WH8bd-Z9l-m3lbDA,4844
|
|
36
36
|
lemonade/tools/llamacpp/load.py,sha256=DFCvQN548Ch9H8U_rHOiYviinzw6vixb5-V7xLj7XE4,6499
|
|
37
|
-
lemonade/tools/llamacpp/utils.py,sha256=
|
|
37
|
+
lemonade/tools/llamacpp/utils.py,sha256=LZ0xae7tTQG9nP55DLm90PJS8UQEwGJmMIb_96pWDKE,32397
|
|
38
38
|
lemonade/tools/oga/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
lemonade/tools/oga/bench.py,sha256=PJXv4UchcS2YPwijNzef8DY4DSAKYxIYY1ycHuH3T34,5005
|
|
40
|
-
lemonade/tools/oga/load.py,sha256=
|
|
40
|
+
lemonade/tools/oga/load.py,sha256=BH5ChYbZgeP_ZN4E6HoboJD3kZcUIAPgPEVbgUZpVjQ,33778
|
|
41
41
|
lemonade/tools/oga/utils.py,sha256=F8UVLKlfYcLa2SUqlehar8-jaX2Aw4u58DjHNNvLdOA,17675
|
|
42
42
|
lemonade/tools/quark/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
43
|
lemonade/tools/quark/quark_load.py,sha256=FJ4LJKTToZbHHWVEOBLadae1a3jCnnY4KvXySHbkJMA,5589
|
|
@@ -46,13 +46,13 @@ lemonade/tools/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
46
46
|
lemonade/tools/report/llm_report.py,sha256=bVHhwCINA-Ok2EdSwAsLubsc83N3KWOVuwTguw7jDcE,6676
|
|
47
47
|
lemonade/tools/report/table.py,sha256=ssqy1bZqF-wptNzKEOj6_9REtCNZyXO8R5vakAtg3R4,27973
|
|
48
48
|
lemonade/tools/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
-
lemonade/tools/server/llamacpp.py,sha256=
|
|
50
|
-
lemonade/tools/server/serve.py,sha256=
|
|
49
|
+
lemonade/tools/server/llamacpp.py,sha256=9OneTx78UgvTzvZbdQidiihAN4F-JyfmhwD0bnj5_IU,21090
|
|
50
|
+
lemonade/tools/server/serve.py,sha256=qA0BqYEeRKXtEoS-hG20M_b1WXiiDmyvfEAk72s6XTc,60573
|
|
51
51
|
lemonade/tools/server/tool_calls.py,sha256=xrAlQwKG-nv2xLlf8f9CDSaUbyMn8ZtHkds9iZLG9K8,5230
|
|
52
|
-
lemonade/tools/server/tray.py,sha256=
|
|
52
|
+
lemonade/tools/server/tray.py,sha256=a9z6hdqlfj91H00j6hAExRPQkzWHhE3dnqSumzEgq0U,19599
|
|
53
53
|
lemonade/tools/server/webapp.py,sha256=8Das5yXOaSBLZmSZ_eddJajQFxBhvl5D6GI_hHlGbE0,1040
|
|
54
54
|
lemonade/tools/server/static/favicon.ico,sha256=hMmP9qGJNeZ0mFS86JIqPbZstXMZn0Z76_HfHQpREAU,126745
|
|
55
|
-
lemonade/tools/server/static/styles.css,sha256=
|
|
55
|
+
lemonade/tools/server/static/styles.css,sha256=5HQQCpm8N_fzLcolPiDuhyZw_5nbO8aIl60xAn4RKmg,43385
|
|
56
56
|
lemonade/tools/server/static/webapp.html,sha256=FX2MZUsljfgxxuF12KBdgvNkso_z-sHewWc0SEGGcGM,18138
|
|
57
57
|
lemonade/tools/server/static/js/chat.js,sha256=BTvREuEt0NrN8qhAuda5tTAoUN6tbsoukevA-zyTrwQ,27193
|
|
58
58
|
lemonade/tools/server/static/js/model-settings.js,sha256=JXHeG7xVrRU181Hj7CZflERAi1Z6t-qwYFR4aH5nf5I,5820
|
|
@@ -62,15 +62,16 @@ lemonade/tools/server/utils/port.py,sha256=J7-g-Aqygb50jNoHLhhRfBZVM-uhGlcB5-oYB
|
|
|
62
62
|
lemonade/tools/server/utils/system_tray.py,sha256=b9lvNv9chJKQxvmH7qzAuUe6H9HsLu7pdHFqGlAJaL0,12654
|
|
63
63
|
lemonade/tools/server/utils/thread.py,sha256=Z-PDzGcpgfN2qxTmtlROWqrUN0B2fXdPrqo_J10fR_w,2772
|
|
64
64
|
lemonade_install/__init__.py,sha256=26zohKg2jgr_5y7tObduWMYQg8zCTWMZHL8lfi2zZVQ,40
|
|
65
|
-
lemonade_install/install.py,sha256=
|
|
66
|
-
lemonade_sdk-8.1.
|
|
67
|
-
lemonade_sdk-8.1.
|
|
68
|
-
lemonade_server/cli.py,sha256=-
|
|
69
|
-
lemonade_server/model_manager.py,sha256=
|
|
70
|
-
lemonade_server/pydantic_models.py,sha256=
|
|
65
|
+
lemonade_install/install.py,sha256=onndA2a-ygyLtDfupI8JQFhU_XpK8McGZtGujFasXww,28304
|
|
66
|
+
lemonade_sdk-8.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
67
|
+
lemonade_sdk-8.1.5.dist-info/licenses/NOTICE.md,sha256=RSca9LE5e6pvdWA_LXAUCcACIHPmINKqkRX-AVRqBGo,3499
|
|
68
|
+
lemonade_server/cli.py,sha256=-oRbLRlOX6SRH1bZURmlkDujsUBwMVprm83MtVR3DEY,18819
|
|
69
|
+
lemonade_server/model_manager.py,sha256=8FsD234ODtqVF3k4GIjo6UnZKSpii11UU_zoIf-kvSU,11133
|
|
70
|
+
lemonade_server/pydantic_models.py,sha256=49MyOlb5feLUlKsGcI75tWaflWckrItqcSVkdCY4e3A,3269
|
|
71
71
|
lemonade_server/server_models.json,sha256=DAdG4ebIt5Dy5MM3kmXn1pO0XbNMph1gdpzbacBDVuc,11664
|
|
72
|
-
|
|
73
|
-
lemonade_sdk-8.1.
|
|
74
|
-
lemonade_sdk-8.1.
|
|
75
|
-
lemonade_sdk-8.1.
|
|
76
|
-
lemonade_sdk-8.1.
|
|
72
|
+
lemonade_server/settings.py,sha256=6nsmPLFJD-UokQDmlx9ZBYMbpnn48So_PuBGWP7Fmfg,1299
|
|
73
|
+
lemonade_sdk-8.1.5.dist-info/METADATA,sha256=DqkuNfUnA3CgSvDSVkz6cQJnQBw75AE9FmbAAoRdrso,16852
|
|
74
|
+
lemonade_sdk-8.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
75
|
+
lemonade_sdk-8.1.5.dist-info/entry_points.txt,sha256=7sRvpNhi1E7amnM7RZo57e8yFF9iA5uuRaIeJ1Xre6w,193
|
|
76
|
+
lemonade_sdk-8.1.5.dist-info/top_level.txt,sha256=10ap5GNiPhalO4V50LRoxA1FqRT9g3Xkia6BITu880k,42
|
|
77
|
+
lemonade_sdk-8.1.5.dist-info/RECORD,,
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
PORTIONS LICENSED AS FOLLOWS
|
|
2
2
|
|
|
3
|
+
## llama.cpp
|
|
4
|
+
|
|
5
|
+
Binaries for llama.cpp are downloaded under the MIT license from https://github.com/ggml-org/llama.cpp, as well as https://github.com/lemonade-sdk/llamacpp-rocm (which uses https://github.com/ggml-org/llama.cpp to build them.)
|
|
6
|
+
|
|
3
7
|
Lemonade SDK used the [ONNX TurnkeyML](https://github.com/onnx/turnkeyml) project as a starting point under the [Apache 2.0 license](./LICENSE).
|
|
4
8
|
|
|
9
|
+
> MIT License
|
|
10
|
+
>
|
|
11
|
+
> Copyright (c) 2023-2024 The ggml authors
|
|
12
|
+
>
|
|
13
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
> in the Software without restriction, including without limitation the rights
|
|
16
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
> furnished to do so, subject to the following conditions:
|
|
19
|
+
>
|
|
20
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
> copies or substantial portions of the Software.
|
|
22
|
+
>
|
|
23
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
> SOFTWARE.
|
|
30
|
+
|
|
5
31
|
## TurnkeyML Attribution
|
|
6
32
|
|
|
7
33
|
TurnkeyML used code from other open source projects as a starting point (see [NOTICE.md](NOTICE.md)). Thank you Philip Colangelo, Derek Elkins, Jeremy Fowers, Dan Gard, Victoria Godsoe, Mark Heaps, Daniel Holanda, Brian Kurtz, Mariah Larwood, Philip Lassen, Andrew Ling, Adrian Macias, Gary Malik, Sarah Massengill, Ashwin Murthy, Hatice Ozen, Tim Sears, Sean Settle, Krishna Sivakumar, Aviv Weinstein, Xueli Xao, Bill Xing, and Lev Zlotnik for your contributions to that work.
|
|
@@ -18,4 +44,4 @@ TurnkeyML used code from other open source projects as a starting point (see [NO
|
|
|
18
44
|
>
|
|
19
45
|
>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
20
46
|
>
|
|
21
|
-
>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
47
|
+
>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
lemonade_server/cli.py
CHANGED
|
@@ -11,6 +11,7 @@ from lemonade_server.pydantic_models import (
|
|
|
11
11
|
DEFAULT_LLAMACPP_BACKEND,
|
|
12
12
|
DEFAULT_CTX_SIZE,
|
|
13
13
|
)
|
|
14
|
+
from lemonade_server.settings import load_setting
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
# Error codes for different CLI scenarios
|
|
@@ -390,12 +391,7 @@ def get_server_info() -> Tuple[int | None, int | None]:
|
|
|
390
391
|
connections = psutil.net_connections(kind="tcp4")
|
|
391
392
|
|
|
392
393
|
for conn in connections:
|
|
393
|
-
if
|
|
394
|
-
conn.status == "LISTEN"
|
|
395
|
-
and conn.laddr
|
|
396
|
-
and conn.laddr.ip in ["127.0.0.1"]
|
|
397
|
-
and conn.pid is not None
|
|
398
|
-
):
|
|
394
|
+
if conn.status == "LISTEN" and conn.laddr and conn.pid is not None:
|
|
399
395
|
if is_lemonade_server(conn.pid):
|
|
400
396
|
return conn.pid, conn.laddr.port
|
|
401
397
|
|
|
@@ -472,6 +468,9 @@ def developer_entrypoint():
|
|
|
472
468
|
def _add_server_arguments(parser):
|
|
473
469
|
"""Add common server arguments to a parser"""
|
|
474
470
|
|
|
471
|
+
# Load the persisted log level to use as a default
|
|
472
|
+
persisted_log_level = load_setting("log_level", DEFAULT_LOG_LEVEL)
|
|
473
|
+
|
|
475
474
|
parser.add_argument(
|
|
476
475
|
"--port",
|
|
477
476
|
type=int,
|
|
@@ -489,7 +488,7 @@ def _add_server_arguments(parser):
|
|
|
489
488
|
type=str,
|
|
490
489
|
help="Log level for the server",
|
|
491
490
|
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
|
492
|
-
default=
|
|
491
|
+
default=persisted_log_level,
|
|
493
492
|
)
|
|
494
493
|
parser.add_argument(
|
|
495
494
|
"--llamacpp",
|
lemonade_server/model_manager.py
CHANGED
|
@@ -103,9 +103,13 @@ class ModelManager:
|
|
|
103
103
|
recipe: Optional[str] = None,
|
|
104
104
|
reasoning: bool = False,
|
|
105
105
|
mmproj: str = "",
|
|
106
|
+
do_not_upgrade: bool = False,
|
|
106
107
|
):
|
|
107
108
|
"""
|
|
108
109
|
Downloads the specified models from Hugging Face.
|
|
110
|
+
|
|
111
|
+
do_not_upgrade: prioritize any local copy of the model over any updated copy
|
|
112
|
+
from the Hugging Face Hub.
|
|
109
113
|
"""
|
|
110
114
|
for model in models:
|
|
111
115
|
if model not in self.supported_models:
|
|
@@ -174,9 +178,15 @@ class ModelManager:
|
|
|
174
178
|
print(f"Downloading {model} ({checkpoint_to_download})")
|
|
175
179
|
|
|
176
180
|
if "gguf" in checkpoint_to_download.lower():
|
|
177
|
-
download_gguf(
|
|
181
|
+
download_gguf(
|
|
182
|
+
gguf_model_config.checkpoint,
|
|
183
|
+
gguf_model_config.mmproj,
|
|
184
|
+
do_not_upgrade=do_not_upgrade,
|
|
185
|
+
)
|
|
178
186
|
else:
|
|
179
|
-
custom_snapshot_download(
|
|
187
|
+
custom_snapshot_download(
|
|
188
|
+
checkpoint_to_download, do_not_upgrade=do_not_upgrade
|
|
189
|
+
)
|
|
180
190
|
|
|
181
191
|
# Register the model in user_models.json, creating that file if needed
|
|
182
192
|
# We do this registration after the download so that we don't register
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from lemonade.cache import DEFAULT_CACHE_DIR
|
|
4
|
+
|
|
5
|
+
# Define the path for the user settings file, placing it in the cache directory
|
|
6
|
+
USER_SETTINGS_FILE = os.path.join(DEFAULT_CACHE_DIR, "user_settings.json")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def save_setting(key, value):
|
|
10
|
+
"""Save a setting to the user_settings.json file."""
|
|
11
|
+
# Ensure the cache directory exists
|
|
12
|
+
os.makedirs(DEFAULT_CACHE_DIR, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
settings = {}
|
|
15
|
+
if os.path.exists(USER_SETTINGS_FILE):
|
|
16
|
+
with open(USER_SETTINGS_FILE, "r") as f:
|
|
17
|
+
try:
|
|
18
|
+
settings = json.load(f)
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
# If the file is empty or corrupt, start with a fresh dictionary
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
settings[key] = value
|
|
24
|
+
with open(USER_SETTINGS_FILE, "w") as f:
|
|
25
|
+
json.dump(settings, f, indent=4)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_setting(key, default=None):
|
|
29
|
+
"""Load a setting from the user_settings.json file."""
|
|
30
|
+
if not os.path.exists(USER_SETTINGS_FILE):
|
|
31
|
+
return default
|
|
32
|
+
|
|
33
|
+
with open(USER_SETTINGS_FILE, "r") as f:
|
|
34
|
+
try:
|
|
35
|
+
settings = json.load(f)
|
|
36
|
+
return settings.get(key, default)
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
# Return default if the file is empty or corrupt
|
|
39
|
+
return default
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|