lemonade-sdk 8.1.9__py3-none-any.whl → 8.1.11__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.

Files changed (33) hide show
  1. lemonade/common/inference_engines.py +13 -4
  2. lemonade/common/system_info.py +570 -1
  3. lemonade/tools/flm/__init__.py +1 -0
  4. lemonade/tools/flm/utils.py +255 -0
  5. lemonade/tools/llamacpp/utils.py +62 -13
  6. lemonade/tools/server/flm.py +137 -0
  7. lemonade/tools/server/llamacpp.py +23 -5
  8. lemonade/tools/server/serve.py +292 -135
  9. lemonade/tools/server/static/js/chat.js +165 -82
  10. lemonade/tools/server/static/js/models.js +87 -54
  11. lemonade/tools/server/static/js/shared.js +5 -3
  12. lemonade/tools/server/static/logs.html +47 -0
  13. lemonade/tools/server/static/styles.css +159 -8
  14. lemonade/tools/server/static/webapp.html +28 -10
  15. lemonade/tools/server/tray.py +158 -38
  16. lemonade/tools/server/utils/macos_tray.py +226 -0
  17. lemonade/tools/server/utils/{system_tray.py → windows_tray.py} +13 -0
  18. lemonade/tools/server/webapp.py +4 -1
  19. lemonade/tools/server/wrapped_server.py +91 -25
  20. lemonade/version.py +1 -1
  21. lemonade_install/install.py +25 -2
  22. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/METADATA +9 -6
  23. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/RECORD +33 -28
  24. lemonade_server/cli.py +105 -14
  25. lemonade_server/model_manager.py +186 -45
  26. lemonade_server/pydantic_models.py +25 -1
  27. lemonade_server/server_models.json +162 -62
  28. lemonade_server/settings.py +39 -39
  29. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/WHEEL +0 -0
  30. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/entry_points.txt +0 -0
  31. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/licenses/LICENSE +0 -0
  32. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/licenses/NOTICE.md +0 -0
  33. {lemonade_sdk-8.1.9.dist-info → lemonade_sdk-8.1.11.dist-info}/top_level.txt +0 -0
@@ -7,11 +7,28 @@ import webbrowser
7
7
  from pathlib import Path
8
8
  import logging
9
9
  import tempfile
10
+ import platform
11
+
10
12
  import requests
11
13
  from packaging.version import parse as parse_version
12
14
 
15
+ from lemonade_server.pydantic_models import DEFAULT_CTX_SIZE
16
+
13
17
  from lemonade.version import __version__
14
- from lemonade.tools.server.utils.system_tray import SystemTray, Menu, MenuItem
18
+
19
+ # Import the appropriate tray implementation based on platform
20
+ if platform.system() == "Darwin": # macOS
21
+ from lemonade.tools.server.utils.macos_tray import (
22
+ MacOSSystemTray as SystemTray,
23
+ Menu,
24
+ MenuItem,
25
+ )
26
+ else: # Windows/Linux
27
+ from lemonade.tools.server.utils.windows_tray import (
28
+ SystemTray,
29
+ Menu,
30
+ MenuItem,
31
+ )
15
32
 
16
33
 
17
34
  class OutputDuplicator:
@@ -57,6 +74,7 @@ class LemonadeTray(SystemTray):
57
74
  self.executor = ThreadPoolExecutor(max_workers=1)
58
75
  self.log_file = log_file
59
76
  self.port = port
77
+ self.ctx_size = DEFAULT_CTX_SIZE
60
78
  self.server_factory = server_factory
61
79
  self.debug_logs_enabled = log_level == "debug"
62
80
 
@@ -83,6 +101,9 @@ class LemonadeTray(SystemTray):
83
101
  self.version_check_thread = None
84
102
  self.stop_version_check = threading.Event()
85
103
 
104
+ # Hook function for platform-specific initialization callback
105
+ self.on_ready = None
106
+
86
107
  def get_latest_version(self):
87
108
  """
88
109
  Update the latest version information.
@@ -187,15 +208,38 @@ class LemonadeTray(SystemTray):
187
208
  Show the log file in a new window.
188
209
  """
189
210
  try:
190
- subprocess.Popen(
191
- [
192
- "powershell",
193
- "Start-Process",
194
- "powershell",
195
- "-ArgumentList",
196
- f'"-NoExit", "Get-Content -Wait {self.log_file}"',
197
- ]
198
- )
211
+ system = platform.system().lower()
212
+ if system == "darwin":
213
+ # Use Terminal.app to show live logs on macOS
214
+ try:
215
+ subprocess.Popen(
216
+ [
217
+ "osascript",
218
+ "-e",
219
+ f'tell application "Terminal" to do script "tail -f {self.log_file}"',
220
+ ]
221
+ )
222
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
223
+ self.logger.error(f"Failed to open Terminal for logs: {e}")
224
+ self.show_balloon_notification(
225
+ "Error",
226
+ f"Failed to open logs in Terminal. Log file: {self.log_file}",
227
+ )
228
+ elif system == "windows":
229
+ # Use PowerShell on Windows
230
+ subprocess.Popen(
231
+ [
232
+ "powershell",
233
+ "Start-Process",
234
+ "powershell",
235
+ "-ArgumentList",
236
+ f'"-NoExit", "Get-Content -Wait {self.log_file}"',
237
+ ]
238
+ )
239
+ else:
240
+ # Unsupported platform
241
+ self.logger.error(f"Log viewing not supported on platform: {system}")
242
+
199
243
  except Exception as e: # pylint: disable=broad-exception-caught
200
244
  self.logger.error(f"Error opening logs: {str(e)}")
201
245
 
@@ -224,7 +268,7 @@ class LemonadeTray(SystemTray):
224
268
  try:
225
269
  response = requests.get(
226
270
  f"http://localhost:{self.port}/api/v0/health",
227
- timeout=0.1, # Add timeout
271
+ timeout=0.1,
228
272
  )
229
273
  response.raise_for_status()
230
274
  response_data = response.json()
@@ -253,7 +297,9 @@ class LemonadeTray(SystemTray):
253
297
  """
254
298
  Change the server port and restart the server.
255
299
  """
300
+
256
301
  try:
302
+
257
303
  # Stop the current server
258
304
  if self.server_thread and self.server_thread.is_alive():
259
305
  # Set should_exit flag on the uvicorn server instance
@@ -266,22 +312,58 @@ class LemonadeTray(SystemTray):
266
312
 
267
313
  # Update the port in both the tray and the server instance
268
314
  self.port = new_port
269
- if self.server:
270
- self.server.port = new_port
271
315
 
272
- # Restart the server
316
+ # Clear the old server instance to ensure a fresh start
317
+ # This prevents middleware conflicts when restarting
318
+ self.server = None
319
+
273
320
  self.server_thread = threading.Thread(target=self.start_server, daemon=True)
274
321
  self.server_thread.start()
275
322
 
276
- # Show notification
277
323
  self.show_balloon_notification(
278
- "Port Changed", f"Lemonade Server is now running on port {self.port}"
324
+ "Port Changed",
325
+ f"Lemonade Server is now running on port {self.port}",
279
326
  )
280
327
 
281
328
  except Exception as e: # pylint: disable=broad-exception-caught
282
329
  self.logger.error(f"Error changing port: {str(e)}")
283
330
  self.show_balloon_notification("Error", f"Failed to change port: {str(e)}")
284
331
 
332
+ def change_context_size(self, _, __, new_ctx_size):
333
+ """
334
+ Change the server context size and restart the server.
335
+ """
336
+ try:
337
+ # Stop the current server
338
+ if self.server_thread and self.server_thread.is_alive():
339
+ # Set should_exit flag on the uvicorn server instance
340
+ if (
341
+ hasattr(self.server, "uvicorn_server")
342
+ and self.server.uvicorn_server
343
+ ):
344
+ self.server.uvicorn_server.should_exit = True
345
+ self.server_thread.join(timeout=2)
346
+ # Update the context size in both the tray and the server instance
347
+ self.ctx_size = new_ctx_size
348
+ if self.server:
349
+ self.server.ctx_size = new_ctx_size
350
+ # Restart the server
351
+ self.server_thread = threading.Thread(target=self.start_server, daemon=True)
352
+ self.server_thread.start()
353
+ # Show notification
354
+ ctx_size_label = (
355
+ f"{new_ctx_size//1024}K" if new_ctx_size >= 1024 else str(new_ctx_size)
356
+ )
357
+ self.show_balloon_notification(
358
+ "Context Size Changed",
359
+ f"Lemonade Server context size is now {ctx_size_label}",
360
+ )
361
+ except Exception as e: # pylint: disable=broad-exception-caught
362
+ self.logger.error(f"Error changing context size: {str(e)}")
363
+ self.show_balloon_notification(
364
+ "Error", f"Failed to change context size: {str(e)}"
365
+ )
366
+
285
367
  def _using_installer(self):
286
368
  """
287
369
  Check if the user is using the NSIS installer by checking for embeddable python
@@ -438,6 +520,30 @@ class LemonadeTray(SystemTray):
438
520
 
439
521
  port_submenu = Menu(*port_menu_items)
440
522
 
523
+ # Create context size selection submenu with 6 options
524
+ ctx_size_menu_items = []
525
+ ctx_size_options = [
526
+ ("4K", 4096),
527
+ ("8K", 8192),
528
+ ("16K", 16384),
529
+ ("32K", 32768),
530
+ ("64K", 65536),
531
+ ("128K", 131072),
532
+ ]
533
+
534
+ for ctx_label, ctx_value in ctx_size_options:
535
+ # Create a function that returns the lambda to properly capture the ctx_size variable
536
+ def create_ctx_handler(ctx_size):
537
+ return lambda icon, item: self.change_context_size(icon, item, ctx_size)
538
+
539
+ ctx_item = MenuItem(
540
+ f"Context size {ctx_label}", create_ctx_handler(ctx_value)
541
+ )
542
+ ctx_item.checked = ctx_value == self.ctx_size
543
+ ctx_size_menu_items.append(ctx_item)
544
+
545
+ ctx_size_submenu = Menu(*ctx_size_menu_items)
546
+
441
547
  # Create the Logs submenu
442
548
  debug_log_text = "Enable Debug Logs"
443
549
  debug_log_item = MenuItem(debug_log_text, self.toggle_debug_logs)
@@ -452,6 +558,7 @@ class LemonadeTray(SystemTray):
452
558
  if status_successfully_checked:
453
559
  items.append(MenuItem("Load Model", None, submenu=load_submenu))
454
560
  items.append(MenuItem("Port", None, submenu=port_submenu))
561
+ items.append(MenuItem("Context Size", None, submenu=ctx_size_submenu))
455
562
  items.append(Menu.SEPARATOR)
456
563
 
457
564
  # Only show upgrade option if newer version is available
@@ -475,6 +582,11 @@ class LemonadeTray(SystemTray):
475
582
  Start the uvicorn server.
476
583
  """
477
584
  self.server = self.server_factory()
585
+
586
+ # Ensure the server uses the current port from the tray
587
+ # This is important when changing ports
588
+ self.server.port = self.port
589
+
478
590
  self.server.uvicorn_server = self.server.run_in_thread(self.server.host)
479
591
  self.server.uvicorn_server.run()
480
592
 
@@ -483,16 +595,6 @@ class LemonadeTray(SystemTray):
483
595
  Run the Lemonade tray application.
484
596
  """
485
597
 
486
- # Register window class and create window
487
- self.register_window_class()
488
- self.create_window()
489
-
490
- # Set up Windows console control handler for CTRL+C
491
- self.console_handler = self.setup_console_control_handler(self.logger)
492
-
493
- # Add tray icon
494
- self.add_tray_icon()
495
-
496
598
  # Start the background model mapping update thread
497
599
  self.model_update_thread = threading.Thread(
498
600
  target=self.update_downloaded_models_background, daemon=True
@@ -509,17 +611,27 @@ class LemonadeTray(SystemTray):
509
611
  self.server_thread = threading.Thread(target=self.start_server, daemon=True)
510
612
  self.server_thread.start()
511
613
 
512
- # Show initial notification
513
- self.show_balloon_notification(
514
- "Woohoo!",
515
- (
516
- "Lemonade Server is running! "
517
- "Right-click the tray icon below to access options."
518
- ),
519
- )
614
+ # Provide an on_ready hook that Windows base tray will call after
615
+ # the HWND/icon are created. macOS will call it immediately after run.
616
+ def _on_ready():
617
+ system = platform.system().lower()
618
+ if system == "darwin":
619
+ message = (
620
+ "Lemonade Server is running! "
621
+ "Click the tray icon above to access options."
622
+ )
623
+ else: # Windows/Linux
624
+ message = (
625
+ "Lemonade Server is running! "
626
+ "Right-click the tray icon below to access options."
627
+ )
628
+ self.show_balloon_notification("Woohoo!", message)
520
629
 
521
- # Run the message loop in the main thread
522
- self.message_loop()
630
+ # Attach hook for both implementations to invoke after init
631
+ self.on_ready = _on_ready
632
+
633
+ # Call the parent run method which handles platform-specific initialization
634
+ super().run()
523
635
 
524
636
  def exit_app(self, icon, item):
525
637
  """
@@ -534,8 +646,16 @@ class LemonadeTray(SystemTray):
534
646
  if self.version_check_thread and self.version_check_thread.is_alive():
535
647
  self.version_check_thread.join(timeout=1)
536
648
 
537
- # Call parent exit method
538
- super().exit_app(icon, item)
649
+ # Platform-specific exit handling
650
+ system = platform.system().lower()
651
+ if system == "darwin": # macOS
652
+ # For macOS, quit the rumps application
653
+ import rumps
654
+
655
+ rumps.quit_application()
656
+ else:
657
+ # Call parent exit method for Windows
658
+ super().exit_app(icon, item)
539
659
 
540
660
  # Stop the server using the CLI stop command to ensure a rigorous cleanup
541
661
  # This must be a subprocess to ensure the cleanup doesnt kill itself
@@ -0,0 +1,226 @@
1
+ import platform
2
+ import subprocess
3
+ from typing import Callable, Optional
4
+
5
+ # Check if we're on macOS and import accordingly
6
+ if platform.system() == "Darwin":
7
+ try:
8
+ import rumps
9
+
10
+ RUMPS_AVAILABLE = True
11
+ except ImportError:
12
+ RUMPS_AVAILABLE = False
13
+ print("Warning: rumps not available. Install with: pip install rumps")
14
+ else:
15
+ RUMPS_AVAILABLE = False
16
+
17
+
18
+ class MenuItem:
19
+ """
20
+ Cross-platform menu item representation.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ text: str,
26
+ callback: Optional[Callable] = None,
27
+ enabled: bool = True,
28
+ submenu=None,
29
+ checked: bool = False,
30
+ ):
31
+ self.text = text
32
+ self.callback = callback
33
+ self.enabled = enabled
34
+ self.submenu = submenu
35
+ self.checked = checked
36
+
37
+
38
+ class Menu:
39
+ """
40
+ Cross-platform menu representation.
41
+ """
42
+
43
+ SEPARATOR = "SEPARATOR"
44
+
45
+ def __init__(self, *items):
46
+ self.items = list(items)
47
+
48
+
49
+ class MacOSSystemTray:
50
+ """
51
+ macOS-specific system tray implementation using rumps.
52
+ """
53
+
54
+ def __init__(self, app_name: str, icon_path: str):
55
+ self._check_rumps_availability()
56
+
57
+ self.app_name = app_name
58
+ self.icon_path = icon_path
59
+ self.app = None
60
+ self.menu_callbacks = {}
61
+ self._menu_update_timer = None
62
+
63
+ def _check_rumps_availability(self):
64
+ """Check if rumps is available and raise error if not."""
65
+ if not RUMPS_AVAILABLE:
66
+ raise ImportError("rumps library is required for macOS tray support")
67
+
68
+ def create_menu(self):
69
+ """
70
+ Create the context menu based on current state. Override in subclass.
71
+ """
72
+ return Menu(MenuItem("Exit", self.exit_app))
73
+
74
+ def build_rumps_menu(self, menu_items):
75
+ """
76
+ Convert our menu structure to rumps menu items.
77
+ """
78
+ rumps_items = []
79
+
80
+ for item in menu_items:
81
+ if item == Menu.SEPARATOR:
82
+ rumps_items.append(rumps.separator)
83
+ elif isinstance(item, MenuItem):
84
+ if item.submenu:
85
+ # Create submenu
86
+ submenu_items = self.build_rumps_menu(item.submenu.items)
87
+ submenu = rumps.MenuItem(item.text)
88
+ for sub_item in submenu_items:
89
+ submenu.add(sub_item)
90
+ rumps_items.append(submenu)
91
+ else:
92
+ # Create regular menu item
93
+ menu_item = rumps.MenuItem(
94
+ item.text,
95
+ callback=(
96
+ self._create_callback_wrapper(item)
97
+ if item.callback
98
+ else None
99
+ ),
100
+ )
101
+
102
+ # Set enabled state
103
+ if not item.enabled:
104
+ menu_item.set_callback(None)
105
+
106
+ # Set checked state
107
+ if item.checked:
108
+ menu_item.state = 1
109
+ else:
110
+ menu_item.state = 0
111
+
112
+ rumps_items.append(menu_item)
113
+
114
+ return rumps_items
115
+
116
+ def _create_callback_wrapper(self, item):
117
+ """Create a callback wrapper that matches our interface."""
118
+
119
+ def wrapper(sender): # pylint: disable=unused-argument
120
+ if item.callback:
121
+ item.callback(None, item)
122
+
123
+ return wrapper
124
+
125
+ def show_balloon_notification(
126
+ self, title, message, timeout=5000
127
+ ): # pylint: disable=unused-argument
128
+ """
129
+ Show a notification on macOS using the Notification Center.
130
+ Falls back to console output if AppleScript fails.
131
+ """
132
+ try:
133
+ # Escape quotes in message and title for AppleScript
134
+ escaped_title = title.replace('"', '\\"')
135
+ escaped_message = message.replace('"', '\\"')
136
+ escaped_app_name = self.app_name.replace('"', '\\"')
137
+
138
+ # Use AppleScript to show notification
139
+ script = (
140
+ f'display notification "{escaped_message}" '
141
+ f'with title "{escaped_title}" subtitle "{escaped_app_name}"'
142
+ )
143
+ subprocess.run(
144
+ ["osascript", "-e", script], check=True, capture_output=True, text=True
145
+ )
146
+ except FileNotFoundError:
147
+ # osascript not available, fallback to console
148
+ print(f"[{self.app_name}] {title}: {message}")
149
+ except subprocess.CalledProcessError as e:
150
+ # AppleScript failed, fallback to console
151
+ print(f"[{self.app_name}] {title}: {message}")
152
+ print(f"Warning: Failed to show notification via AppleScript: {e}")
153
+ except Exception as e: # pylint: disable=broad-exception-caught
154
+ # Any other error, fallback to console
155
+ print(f"[{self.app_name}] {title}: {message}")
156
+ print(f"Warning: Failed to show notification: {e}")
157
+
158
+ def exit_app(self, _, __):
159
+ """Exit the application."""
160
+ if self.app:
161
+ rumps.quit_application()
162
+
163
+ def run(self):
164
+ """
165
+ Run the tray application.
166
+ """
167
+ self._check_rumps_availability()
168
+
169
+ try:
170
+ # Create the rumps app
171
+ self.app = rumps.App(self.app_name, icon=self.icon_path, quit_button=None)
172
+
173
+ # Build the initial menu
174
+ self.refresh_menu()
175
+
176
+ # Set up a timer to refresh menu periodically (every 3 seconds)
177
+ # This provides a good balance between responsiveness and performance
178
+ self._setup_menu_refresh_timer()
179
+
180
+ # Call the on_ready hook if available (for compatibility with tray.py)
181
+ if hasattr(self, "on_ready") and callable(getattr(self, "on_ready", None)):
182
+ getattr(self, "on_ready")()
183
+
184
+ # Start the app
185
+ self.app.run()
186
+ except Exception as e:
187
+ raise RuntimeError(f"Failed to start macOS tray application: {e}") from e
188
+
189
+ def refresh_menu(self):
190
+ """
191
+ Refresh the menu by rebuilding it with current state.
192
+ """
193
+ if not self.app:
194
+ return
195
+
196
+ # Clear existing menu
197
+ self.app.menu.clear()
198
+
199
+ # Build fresh menu with current state
200
+ menu = self.create_menu()
201
+ menu_items = self.build_rumps_menu(menu.items)
202
+
203
+ # Add updated menu items
204
+ for item in menu_items:
205
+ self.app.menu.add(item)
206
+
207
+ def _setup_menu_refresh_timer(self):
208
+ """
209
+ Set up a timer to periodically refresh the menu.
210
+ """
211
+ if not self.app:
212
+ return
213
+
214
+ # Create a timer that refreshes the menu every 3 seconds
215
+ @rumps.timer(3)
216
+ def refresh_menu_timer(sender): # pylint: disable=unused-argument
217
+ self.refresh_menu()
218
+
219
+ # Store reference to prevent garbage collection
220
+ self._menu_update_timer = refresh_menu_timer
221
+
222
+ def update_menu(self):
223
+ """
224
+ Update the menu by rebuilding it.
225
+ """
226
+ self.refresh_menu()
@@ -25,12 +25,14 @@ class MenuItem:
25
25
  enabled: bool = True,
26
26
  submenu=None,
27
27
  bitmap_path: Optional[str] = None,
28
+ checked: bool = False,
28
29
  ):
29
30
  self.text = text
30
31
  self.callback = callback
31
32
  self.enabled = enabled
32
33
  self.submenu = submenu
33
34
  self.bitmap_path = bitmap_path
35
+ self.checked = checked
34
36
  self.id = None # Will be set when menu is created
35
37
  self.bitmap_handle = None
36
38
 
@@ -358,9 +360,20 @@ class SystemTray:
358
360
  self.register_window_class()
359
361
  self.create_window()
360
362
 
363
+ # Set up Windows console control handler for CTRL+C
364
+ self.console_handler = self.setup_console_control_handler(self.logger)
365
+
361
366
  # Add tray icon
362
367
  self.add_tray_icon()
363
368
 
369
+ # Notify subclasses that the tray is ready (hwnd and icon created)
370
+ # Allows showing initial notifications after initialization
371
+ try:
372
+ if hasattr(self, "on_ready") and callable(getattr(self, "on_ready")):
373
+ self.on_ready()
374
+ except Exception:
375
+ pass
376
+
364
377
  # Run the message loop in the main thread
365
378
  self.message_loop()
366
379
 
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
  import json
3
+ import platform
3
4
  from fastapi.responses import HTMLResponse
4
5
  from lemonade_server.model_manager import ModelManager
5
6
 
@@ -14,10 +15,11 @@ def get_webapp_html(port=8000):
14
15
  # Use shared filter function from model_manager.py
15
16
  filtered_models = ModelManager().filter_models_by_backend(server_models)
16
17
 
17
- # Pass filtered server_models to JS
18
+ # Pass filtered server_models and platform info to JS
18
19
  server_models_js = (
19
20
  f"<script>window.SERVER_MODELS = {json.dumps(filtered_models)};</script>"
20
21
  )
22
+ platform_js = f"<script>window.PLATFORM = '{platform.system()}';</script>"
21
23
 
22
24
  # Load HTML template
23
25
  template_path = Path(__file__).parent / "static" / "webapp.html"
@@ -27,5 +29,6 @@ def get_webapp_html(port=8000):
27
29
  # Replace template variables
28
30
  html_content = html_template.replace("{{SERVER_PORT}}", str(port))
29
31
  html_content = html_content.replace("{{SERVER_MODELS_JS}}", server_models_js)
32
+ html_content = html_content.replace("{{PLATFORM_JS}}", platform_js)
30
33
 
31
34
  return HTMLResponse(content=html_content)