mlx-stack 0.1.0__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.
Files changed (61) hide show
  1. mlx_stack/__init__.py +5 -0
  2. mlx_stack/_version.py +24 -0
  3. mlx_stack/cli/__init__.py +5 -0
  4. mlx_stack/cli/bench.py +221 -0
  5. mlx_stack/cli/config.py +166 -0
  6. mlx_stack/cli/down.py +109 -0
  7. mlx_stack/cli/init.py +180 -0
  8. mlx_stack/cli/install.py +165 -0
  9. mlx_stack/cli/logs.py +234 -0
  10. mlx_stack/cli/main.py +187 -0
  11. mlx_stack/cli/models.py +304 -0
  12. mlx_stack/cli/profile.py +65 -0
  13. mlx_stack/cli/pull.py +134 -0
  14. mlx_stack/cli/recommend.py +397 -0
  15. mlx_stack/cli/status.py +111 -0
  16. mlx_stack/cli/up.py +163 -0
  17. mlx_stack/cli/watch.py +252 -0
  18. mlx_stack/core/__init__.py +1 -0
  19. mlx_stack/core/benchmark.py +1182 -0
  20. mlx_stack/core/catalog.py +560 -0
  21. mlx_stack/core/config.py +471 -0
  22. mlx_stack/core/deps.py +323 -0
  23. mlx_stack/core/hardware.py +304 -0
  24. mlx_stack/core/launchd.py +531 -0
  25. mlx_stack/core/litellm_gen.py +188 -0
  26. mlx_stack/core/log_rotation.py +231 -0
  27. mlx_stack/core/log_viewer.py +386 -0
  28. mlx_stack/core/models.py +639 -0
  29. mlx_stack/core/paths.py +79 -0
  30. mlx_stack/core/process.py +887 -0
  31. mlx_stack/core/pull.py +815 -0
  32. mlx_stack/core/scoring.py +611 -0
  33. mlx_stack/core/stack_down.py +317 -0
  34. mlx_stack/core/stack_init.py +524 -0
  35. mlx_stack/core/stack_status.py +229 -0
  36. mlx_stack/core/stack_up.py +856 -0
  37. mlx_stack/core/watchdog.py +744 -0
  38. mlx_stack/data/__init__.py +1 -0
  39. mlx_stack/data/catalog/__init__.py +1 -0
  40. mlx_stack/data/catalog/deepseek-r1-32b.yaml +46 -0
  41. mlx_stack/data/catalog/deepseek-r1-8b.yaml +45 -0
  42. mlx_stack/data/catalog/gemma3-12b.yaml +45 -0
  43. mlx_stack/data/catalog/gemma3-27b.yaml +45 -0
  44. mlx_stack/data/catalog/gemma3-4b.yaml +45 -0
  45. mlx_stack/data/catalog/llama3.3-8b.yaml +44 -0
  46. mlx_stack/data/catalog/nemotron-49b.yaml +41 -0
  47. mlx_stack/data/catalog/nemotron-8b.yaml +44 -0
  48. mlx_stack/data/catalog/qwen3-8b.yaml +45 -0
  49. mlx_stack/data/catalog/qwen3.5-0.8b.yaml +45 -0
  50. mlx_stack/data/catalog/qwen3.5-14b.yaml +46 -0
  51. mlx_stack/data/catalog/qwen3.5-32b.yaml +45 -0
  52. mlx_stack/data/catalog/qwen3.5-3b.yaml +44 -0
  53. mlx_stack/data/catalog/qwen3.5-72b.yaml +42 -0
  54. mlx_stack/data/catalog/qwen3.5-8b.yaml +45 -0
  55. mlx_stack/py.typed +1 -0
  56. mlx_stack/utils/__init__.py +1 -0
  57. mlx_stack-0.1.0.dist-info/METADATA +397 -0
  58. mlx_stack-0.1.0.dist-info/RECORD +61 -0
  59. mlx_stack-0.1.0.dist-info/WHEEL +4 -0
  60. mlx_stack-0.1.0.dist-info/entry_points.txt +2 -0
  61. mlx_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
mlx_stack/cli/init.py ADDED
@@ -0,0 +1,180 @@
1
+ """CLI command for stack initialization — `mlx-stack init`.
2
+
3
+ Generates stack definition and LiteLLM configuration files from a
4
+ hardware profile and recommendation. Supports --accept-defaults for
5
+ non-interactive mode, --intent, --add/--remove for customization,
6
+ and --force for overwriting existing configs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from mlx_stack.core.stack_init import InitError, run_init
17
+
18
+ console = Console(stderr=True)
19
+
20
+
21
+ def _display_summary(result: dict) -> None:
22
+ """Display a summary of the generated configuration.
23
+
24
+ Shows file paths, tier assignments, total estimated memory,
25
+ and next-step instructions.
26
+
27
+ Args:
28
+ result: The result dict from run_init.
29
+ """
30
+ out = Console()
31
+ stack = result["stack"]
32
+ profile = result["profile"]
33
+
34
+ out.print()
35
+ out.print(Text("✅ Stack initialized successfully!", style="bold green"))
36
+ out.print()
37
+
38
+ # File paths
39
+ out.print(Text("Generated files:", style="bold"))
40
+ out.print(f" Stack: {result['stack_path']}")
41
+ out.print(f" LiteLLM: {result['litellm_path']}")
42
+ out.print()
43
+
44
+ # Tier assignments table
45
+ out.print(Text("Tier assignments:", style="bold"))
46
+ table = Table(show_header=True, header_style="bold cyan")
47
+ table.add_column("Tier", style="bold", min_width=12)
48
+ table.add_column("Model", min_width=20)
49
+ table.add_column("Quant", min_width=6)
50
+ table.add_column("Port", justify="right", min_width=6)
51
+
52
+ for tier in stack["tiers"]:
53
+ table.add_row(
54
+ tier["name"],
55
+ tier["model"],
56
+ tier["quant"],
57
+ str(tier["port"]),
58
+ )
59
+
60
+ out.print(table)
61
+
62
+ # Hardware and memory summary
63
+ out.print()
64
+ budget_gb = result["memory_budget_gb"]
65
+ total_memory_gb = result.get("total_memory_gb", 0.0)
66
+ out.print(
67
+ f"[dim]Hardware: {profile.chip} ({profile.memory_gb} GB) · "
68
+ f"Budget: {budget_gb:.1f} GB[/dim]"
69
+ )
70
+ if total_memory_gb > 0:
71
+ out.print(
72
+ f"[dim]Total estimated memory: {total_memory_gb:.1f} GB[/dim]"
73
+ )
74
+
75
+ # Warnings (e.g., memory budget exceeded with --add)
76
+ init_warnings = result.get("warnings", [])
77
+ if init_warnings:
78
+ out.print()
79
+ for warning in init_warnings:
80
+ out.print(f"[yellow]⚠ {warning}[/yellow]")
81
+
82
+ # Cloud fallback indicator
83
+ if stack.get("cloud_fallback"):
84
+ out.print()
85
+ out.print(
86
+ "[bold green]☁ Cloud Fallback[/bold green] "
87
+ "Premium tier via OpenRouter configured"
88
+ )
89
+
90
+ # Missing models warning
91
+ missing = result.get("missing_models", [])
92
+ if missing:
93
+ out.print()
94
+ out.print("[yellow]⚠ Missing local models:[/yellow]")
95
+ for model_id in missing:
96
+ out.print(f" • {model_id}")
97
+ out.print()
98
+ out.print(
99
+ " Run [bold]mlx-stack pull[/bold] to download missing models "
100
+ "before starting the stack."
101
+ )
102
+
103
+ # Next steps
104
+ out.print()
105
+ out.print(Text("Next steps:", style="bold"))
106
+ if missing:
107
+ out.print(" 1. [bold]mlx-stack pull[/bold] — Download missing models")
108
+ out.print(" 2. [bold]mlx-stack up[/bold] — Start all services")
109
+ else:
110
+ out.print(" 1. [bold]mlx-stack up[/bold] — Start all services")
111
+ out.print()
112
+
113
+
114
+ @click.command()
115
+ @click.option(
116
+ "--accept-defaults",
117
+ is_flag=True,
118
+ default=False,
119
+ help="Use defaults without prompting (balanced intent, default budget).",
120
+ )
121
+ @click.option(
122
+ "--intent",
123
+ type=str,
124
+ default=None,
125
+ help="Recommendation intent: balanced (default) or agent-fleet.",
126
+ )
127
+ @click.option(
128
+ "--add",
129
+ "add_models",
130
+ multiple=True,
131
+ help="Add a model to the stack (can be specified multiple times).",
132
+ )
133
+ @click.option(
134
+ "--remove",
135
+ "remove_tiers",
136
+ multiple=True,
137
+ help="Remove a tier from the stack (can be specified multiple times).",
138
+ )
139
+ @click.option(
140
+ "--force",
141
+ is_flag=True,
142
+ default=False,
143
+ help="Overwrite existing stack configuration.",
144
+ )
145
+ def init(
146
+ accept_defaults: bool,
147
+ intent: str | None,
148
+ add_models: tuple[str, ...],
149
+ remove_tiers: tuple[str, ...],
150
+ force: bool,
151
+ ) -> None:
152
+ """Generate stack definition and LiteLLM config.
153
+
154
+ Creates ~/.mlx-stack/stacks/default.yaml with tier assignments
155
+ and ~/.mlx-stack/litellm.yaml with proxy configuration.
156
+
157
+ Use --accept-defaults for non-interactive mode. Combine with
158
+ --intent to specify the optimization strategy.
159
+
160
+ Use --add to include additional models and --remove to exclude
161
+ specific tiers from the default recommendation.
162
+
163
+ Requires --force to overwrite an existing stack configuration.
164
+ """
165
+ # Default intent
166
+ if intent is None:
167
+ intent = "balanced"
168
+
169
+ try:
170
+ result = run_init(
171
+ intent=intent,
172
+ add_models=list(add_models) if add_models else None,
173
+ remove_tiers=list(remove_tiers) if remove_tiers else None,
174
+ force=force,
175
+ )
176
+ except InitError as exc:
177
+ console.print(f"[bold red]Error:[/bold red] {exc}")
178
+ raise SystemExit(1) from None
179
+
180
+ _display_summary(result)
@@ -0,0 +1,165 @@
1
+ """CLI commands for launchd integration — `mlx-stack install` and `mlx-stack uninstall`.
2
+
3
+ Manages the watchdog as a macOS LaunchAgent, enabling automatic startup
4
+ on login and persistent health monitoring.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.text import Text
14
+
15
+ from mlx_stack.core.launchd import (
16
+ AgentStatus,
17
+ LaunchdError,
18
+ PlatformError,
19
+ PrerequisiteError,
20
+ check_platform,
21
+ get_agent_status,
22
+ install_agent,
23
+ uninstall_agent,
24
+ )
25
+
26
+ console = Console(stderr=True)
27
+
28
+
29
+ # --------------------------------------------------------------------------- #
30
+ # Status display helper
31
+ # --------------------------------------------------------------------------- #
32
+
33
+
34
+ def _display_status(status: AgentStatus) -> None:
35
+ """Display the agent status in Rich format.
36
+
37
+ Args:
38
+ status: The current agent status.
39
+ """
40
+ out = Console()
41
+
42
+ if not status.installed:
43
+ out.print(Text("Status: not installed", style="dim"))
44
+ elif status.running and status.pid is not None:
45
+ out.print(
46
+ Text(f"Status: installed and running (PID {status.pid})", style="green")
47
+ )
48
+ else:
49
+ out.print(Text("Status: installed but not running", style="yellow"))
50
+
51
+
52
+ # --------------------------------------------------------------------------- #
53
+ # install command
54
+ # --------------------------------------------------------------------------- #
55
+
56
+
57
+ @click.command()
58
+ @click.option(
59
+ "--status",
60
+ "show_status",
61
+ is_flag=True,
62
+ default=False,
63
+ help="Show the current launchd agent status without installing.",
64
+ )
65
+ def install(show_status: bool) -> None:
66
+ """Install the watchdog as a macOS LaunchAgent.
67
+
68
+ Generates a launchd plist that runs `mlx-stack watch` automatically
69
+ on login. The watchdog monitors stack health and auto-restarts
70
+ crashed services.
71
+
72
+ \b
73
+ Requires:
74
+ • macOS (launchd is macOS-only)
75
+ • mlx-stack init (stack must be configured first)
76
+
77
+ \b
78
+ Behavior:
79
+ • Creates plist at ~/Library/LaunchAgents/com.mlx-stack.watchdog.plist
80
+ • Loads the agent immediately via launchctl bootstrap
81
+ • If already installed, updates the plist and reloads
82
+
83
+ \b
84
+ Examples:
85
+ mlx-stack install Install and start the watchdog agent
86
+ mlx-stack install --status Check if the agent is installed
87
+ """
88
+ # Platform guard: reject non-macOS before any branch (including --status)
89
+ try:
90
+ check_platform()
91
+ except PlatformError as exc:
92
+ console.print(f"[red]Error:[/red] {exc}")
93
+ sys.exit(1)
94
+
95
+ if show_status:
96
+ status = get_agent_status()
97
+ _display_status(status)
98
+ return
99
+
100
+ try:
101
+ plist_path, was_reinstall = install_agent()
102
+
103
+ out = Console()
104
+ if was_reinstall:
105
+ out.print(Text("Watchdog agent updated and reloaded.", style="bold green"))
106
+ else:
107
+ out.print(Text("Watchdog agent installed successfully.", style="bold green"))
108
+
109
+ out.print(Text(f" Plist: {plist_path}", style="dim"))
110
+ out.print(
111
+ Text(
112
+ " The watchdog will start automatically on login.",
113
+ style="dim",
114
+ )
115
+ )
116
+
117
+ except PlatformError as exc:
118
+ console.print(f"[red]Error:[/red] {exc}")
119
+ sys.exit(1)
120
+ except PrerequisiteError as exc:
121
+ console.print(f"[red]Error:[/red] {exc}")
122
+ sys.exit(1)
123
+ except LaunchdError as exc:
124
+ console.print(f"[red]Error:[/red] {exc}")
125
+ sys.exit(1)
126
+
127
+
128
+ # --------------------------------------------------------------------------- #
129
+ # uninstall command
130
+ # --------------------------------------------------------------------------- #
131
+
132
+
133
+ @click.command()
134
+ def uninstall() -> None:
135
+ """Uninstall the watchdog LaunchAgent.
136
+
137
+ Stops the watchdog agent and removes the launchd plist. Running
138
+ services are not affected — only the watchdog auto-monitoring
139
+ is disabled.
140
+
141
+ \b
142
+ Examples:
143
+ mlx-stack uninstall Remove the watchdog agent
144
+ """
145
+ try:
146
+ removed = uninstall_agent()
147
+
148
+ out = Console()
149
+ if removed:
150
+ out.print(Text("Watchdog agent uninstalled.", style="bold green"))
151
+ out.print(
152
+ Text(
153
+ " Running services are not affected.",
154
+ style="dim",
155
+ )
156
+ )
157
+ else:
158
+ out.print(Text("Watchdog agent is not installed.", style="yellow"))
159
+
160
+ except PlatformError as exc:
161
+ console.print(f"[red]Error:[/red] {exc}")
162
+ sys.exit(1)
163
+ except LaunchdError as exc:
164
+ console.print(f"[red]Error:[/red] {exc}")
165
+ sys.exit(1)
mlx_stack/cli/logs.py ADDED
@@ -0,0 +1,234 @@
1
+ """CLI command for viewing and managing service logs — `mlx-stack logs`.
2
+
3
+ View log content, follow in real-time, list available log files,
4
+ trigger on-demand rotation, and view archived logs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from mlx_stack.core.log_viewer import (
17
+ DEFAULT_TAIL_LINES,
18
+ follow_log,
19
+ get_available_services,
20
+ get_log_path,
21
+ list_log_files,
22
+ read_all_logs,
23
+ read_log_tail,
24
+ rotate_all_logs,
25
+ rotate_service_log,
26
+ )
27
+
28
+ console = Console(stderr=True)
29
+
30
+
31
+ def _display_log_list() -> None:
32
+ """Display a Rich table listing all available log files."""
33
+ log_files = list_log_files()
34
+
35
+ out = Console()
36
+ out.print()
37
+
38
+ if not log_files:
39
+ out.print(
40
+ Text(
41
+ "No log files found in logs directory.",
42
+ style="yellow",
43
+ )
44
+ )
45
+ out.print()
46
+ return
47
+
48
+ table = Table(
49
+ title="Available Log Files",
50
+ show_header=True,
51
+ header_style="bold cyan",
52
+ )
53
+ table.add_column("Name", style="bold", min_width=20)
54
+ table.add_column("Size", justify="right", min_width=10)
55
+ table.add_column("Modified", min_width=20)
56
+
57
+ for info in log_files:
58
+ table.add_row(info.name, info.size_display, info.modified_display)
59
+
60
+ out.print(table)
61
+ out.print()
62
+
63
+
64
+ def _display_rotation_results(results: list) -> None:
65
+ """Display rotation results with Rich formatting.
66
+
67
+ Args:
68
+ results: List of RotationResult objects.
69
+ """
70
+ out = Console()
71
+ out.print()
72
+
73
+ any_rotated = False
74
+ for result in results:
75
+ if result.error:
76
+ out.print(f"[red]✗[/red] {result.service}: {result.error}")
77
+ elif result.rotated:
78
+ out.print(f"[green]✓[/green] {result.service}: rotated")
79
+ any_rotated = True
80
+ else:
81
+ out.print(f"[dim]–[/dim] {result.service}: no rotation needed")
82
+
83
+ if not results:
84
+ out.print(Text("No log files found to rotate.", style="yellow"))
85
+ elif not any_rotated and not any(r.error for r in results):
86
+ out.print()
87
+ out.print(Text("No rotation needed.", style="dim"))
88
+
89
+ out.print()
90
+
91
+
92
+ @click.command()
93
+ @click.argument("service", required=False, default=None)
94
+ @click.option(
95
+ "--tail",
96
+ "tail_lines",
97
+ type=int,
98
+ default=None,
99
+ help=f"Show last N lines (default {DEFAULT_TAIL_LINES}).",
100
+ )
101
+ @click.option(
102
+ "--follow",
103
+ "-f",
104
+ is_flag=True,
105
+ default=False,
106
+ help="Follow log output in real-time (tail -f behavior).",
107
+ )
108
+ @click.option(
109
+ "--service",
110
+ "service_filter",
111
+ type=str,
112
+ default=None,
113
+ help="Filter to a specific service's log.",
114
+ )
115
+ @click.option(
116
+ "--rotate",
117
+ is_flag=True,
118
+ default=False,
119
+ help="Rotate eligible log files.",
120
+ )
121
+ @click.option(
122
+ "--all",
123
+ "show_all",
124
+ is_flag=True,
125
+ default=False,
126
+ help="Show archived and current logs in chronological order.",
127
+ )
128
+ def logs(
129
+ service: str | None,
130
+ tail_lines: int | None,
131
+ follow: bool,
132
+ service_filter: str | None,
133
+ rotate: bool,
134
+ show_all: bool,
135
+ ) -> None:
136
+ """View and manage service logs.
137
+
138
+ Without arguments, lists all available log files with sizes and
139
+ modification times.
140
+
141
+ \b
142
+ Examples:
143
+ mlx-stack logs List all log files
144
+ mlx-stack logs fast Show last 50 lines of fast.log
145
+ mlx-stack logs fast --tail 100 Show last 100 lines
146
+ mlx-stack logs fast --follow Stream new log content in real-time
147
+ mlx-stack logs --rotate Rotate all eligible log files
148
+ mlx-stack logs fast --all Show archived + current logs
149
+ """
150
+ # Resolve effective service name: argument takes precedence over --service
151
+ effective_service = service or service_filter
152
+
153
+ # Handle --rotate mode
154
+ if rotate:
155
+ if effective_service:
156
+ # Rotate only the specified service
157
+ available = get_available_services()
158
+ if available and effective_service not in available:
159
+ console.print(
160
+ f"[red]Error:[/red] No log file found for service "
161
+ f"'{effective_service}'. "
162
+ f"Available services: {', '.join(available)}"
163
+ )
164
+ sys.exit(1)
165
+
166
+ result = rotate_service_log(effective_service)
167
+ _display_rotation_results([result])
168
+ else:
169
+ # Rotate all eligible logs
170
+ results = rotate_all_logs()
171
+ _display_rotation_results(results)
172
+ return
173
+
174
+ # No service specified — list log files
175
+ if effective_service is None:
176
+ _display_log_list()
177
+ return
178
+
179
+ # Handle --all mode: show archives + current.
180
+ # This must be checked BEFORE the log_path validation because
181
+ # read_all_logs works even when the current log file is missing
182
+ # (it can show archives only).
183
+ if show_all:
184
+ content = read_all_logs(effective_service)
185
+ if content:
186
+ click.echo(content)
187
+ else:
188
+ console.print(
189
+ Text(
190
+ f"No log content found for service '{effective_service}'.",
191
+ style="yellow",
192
+ )
193
+ )
194
+ return
195
+
196
+ # Validate service name — required for --follow, --tail, and default modes
197
+ log_path = get_log_path(effective_service)
198
+ if log_path is None:
199
+ available = get_available_services()
200
+ if available:
201
+ console.print(
202
+ f"[red]Error:[/red] No log file found for service "
203
+ f"'{effective_service}'. "
204
+ f"Available services: {', '.join(available)}"
205
+ )
206
+ else:
207
+ console.print(
208
+ f"[red]Error:[/red] No log file found for service "
209
+ f"'{effective_service}'. No log files exist."
210
+ )
211
+ sys.exit(1)
212
+
213
+ # Handle --follow mode
214
+ if follow:
215
+ num = tail_lines if tail_lines is not None else DEFAULT_TAIL_LINES
216
+ try:
217
+ follow_log(log_path, num_lines=num, output_callback=click.echo)
218
+ except KeyboardInterrupt:
219
+ # Belt-and-suspenders: ensure clean exit
220
+ pass
221
+ return
222
+
223
+ # Default: show tail of log
224
+ num = tail_lines if tail_lines is not None else DEFAULT_TAIL_LINES
225
+ content = read_log_tail(log_path, num_lines=num)
226
+ if content:
227
+ click.echo(content)
228
+ else:
229
+ console.print(
230
+ Text(
231
+ f"Log file for service '{effective_service}' is empty.",
232
+ style="yellow",
233
+ )
234
+ )