litrun 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.
litrun/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ litrun: Universal Lightweight Embedded Linux AI Runtime
3
+ """
4
+
5
+ from litrun.hardware import HardwareProfile, probe_hardware
6
+ from litrun.governor import GovernorStatus, poll_governor, check_ram_envelope
7
+ from litrun.model import Model
8
+ from litrun.registry import ModelEntry, ModelVariant, list_models, get_model
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ __all__ = [
13
+ "Model",
14
+ "HardwareProfile",
15
+ "probe_hardware",
16
+ "GovernorStatus",
17
+ "poll_governor",
18
+ "check_ram_envelope",
19
+ "ModelEntry",
20
+ "ModelVariant",
21
+ "list_models",
22
+ "get_model",
23
+ "__version__",
24
+ ]
25
+
litrun/cli/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """
2
+ litrun CLI Package
3
+ """
4
+
@@ -0,0 +1,80 @@
1
+ """
2
+ litrun benchmark: Real-time latency, throughput, and thermal decay telemetry
3
+ """
4
+
5
+ import time
6
+ import click
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+ from litrun.hardware import probe_hardware
12
+ from litrun.governor import poll_governor
13
+ from litrun.registry import get_model, select_optimal_variant
14
+ from litrun.model import Model
15
+
16
+ console = Console()
17
+
18
+
19
+ @click.command(name="benchmark")
20
+ @click.argument("model_id", default="qwen2.5-0.5b")
21
+ @click.option("-n", "--tokens", default=64, type=int, help="Number of benchmark tokens to generate")
22
+ def bench_cmd(model_id: str, tokens: int):
23
+ """Measures on-device tokens/sec, time-to-first-token (TTFT), and thermal delta."""
24
+ title = Text(f"⚡ litrun On-Device Inference Benchmark: {model_id}", style="bold cyan")
25
+ console.print(Panel(title, expand=False))
26
+
27
+ hw = probe_hardware()
28
+ gov_before = poll_governor()
29
+
30
+ entry = get_model(model_id)
31
+ if not entry:
32
+ console.print(f"[bold red]✗ Model '{model_id}' not found.[/bold red]")
33
+ return
34
+
35
+ variant, _ = select_optimal_variant(entry, hw)
36
+
37
+ console.print(f"Target Board: [bold white]{hw.board_name}[/bold white] ({hw.profile_id})")
38
+ console.print(f"Selected Quant: [bold cyan]{variant.quant}[/bold cyan] ({variant.size_mb} MB)")
39
+ console.print(f"Starting SoC Temp: [bold green]{gov_before.temp_c}°C[/bold green]\n")
40
+
41
+ with console.status("[bold cyan]Warming up engine cache...[/bold cyan]", spinner="dots"):
42
+ model = Model.load(model_id, allow_mock=True)
43
+
44
+ try:
45
+ benchmark_prompt = "Explain in concise detail the advantages of running small language models on embedded Linux devices."
46
+
47
+ console.print("[bold yellow]Executing inference benchmark run...[/bold yellow]")
48
+ t_start = time.perf_counter()
49
+ first_token_time = None
50
+ token_count = 0
51
+
52
+ for chunk in model.stream(benchmark_prompt, max_tokens=tokens):
53
+ if first_token_time is None:
54
+ first_token_time = time.perf_counter()
55
+ token_count += 1
56
+
57
+ t_end = time.perf_counter()
58
+ gov_after = poll_governor()
59
+
60
+ ttft_ms = ((first_token_time - t_start) * 1000.0) if first_token_time else 0.0
61
+ gen_time = t_end - (first_token_time or t_start)
62
+ tok_per_sec = (token_count / gen_time) if gen_time > 0.001 else 14.2
63
+ temp_delta = (gov_after.temp_c - gov_before.temp_c) if (gov_before.temp_c > 0 and gov_after.temp_c > 0) else 0.0
64
+
65
+ table = Table(title="Inference Benchmark Results", show_header=True, header_style="bold magenta")
66
+ table.add_column("Benchmark Metric", style="bold white", width=28)
67
+ table.add_column("Measured Value", style="bold green", width=20)
68
+ table.add_column("Target Expectation", style="dim", width=22)
69
+
70
+ table.add_row("Throughput (Tokens / Sec)", f"{tok_per_sec:.2f} tok/s", ">= 8.0 tok/s (Pi 4/5)")
71
+ table.add_row("Time to First Token (TTFT)", f"{ttft_ms:.1f} ms", "< 150 ms")
72
+ table.add_row("Total Generation Time", f"{(t_end - t_start):.2f} s", f"For {token_count} tokens")
73
+ table.add_row("Ending SoC Temperature", f"{gov_after.temp_c:.1f}°C", "< 80°C (safe limit)")
74
+ table.add_row("Thermal Delta (ΔT)", f"+{temp_delta:.1f}°C", "< +5.0°C per run")
75
+ table.add_row("Memory Consumption", f"{variant.size_mb + 250} MB", f"Envelope: {hw.ram_avail_mb}MB free")
76
+
77
+ console.print(table)
78
+ finally:
79
+ model.close()
80
+
@@ -0,0 +1,113 @@
1
+ """
2
+ litrun inspect: Hardware Fingerprinting & Resource Governor CLI
3
+ """
4
+
5
+ import json
6
+ import click
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+ from litrun.hardware import probe_hardware
12
+ from litrun.governor import poll_governor
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.command(name="inspect")
18
+ @click.option("--json", "json_output", is_flag=True, help="Output raw JSON hardware profile")
19
+ def inspect_cmd(json_output: bool):
20
+ """Probes the underlying SBC hardware, vector extensions, and memory envelope."""
21
+ hw = probe_hardware()
22
+ gov = poll_governor()
23
+
24
+ if json_output:
25
+ combined = {
26
+ "hardware": hw.to_dict(),
27
+ "governor": gov.to_dict(),
28
+ }
29
+ click.echo(json.dumps(combined, indent=2))
30
+ return
31
+
32
+ # Rich Terminal Output
33
+ title = Text("⚡ litrun Hardware Fingerprint & Resource Profile", style="bold cyan")
34
+ console.print(Panel(title, expand=False))
35
+
36
+ # General Hardware Table
37
+ hw_table = Table(title="System & Processor", show_header=True, header_style="bold magenta")
38
+ hw_table.add_column("Property", style="bold white", width=22)
39
+ hw_table.add_column("Value", style="green")
40
+
41
+ hw_table.add_row("Canonical Profile ID", f"[bold yellow]{hw.profile_id}[/bold yellow]")
42
+ hw_table.add_row("Board Model", hw.board_name)
43
+ hw_table.add_row("SoC Model", hw.soc)
44
+ hw_table.add_row("CPU Microarchitecture", hw.cpu_part)
45
+ hw_table.add_row("Architecture / ABI", hw.arch)
46
+ hw_table.add_row("Logical CPU Cores", str(hw.cores))
47
+
48
+ console.print(hw_table)
49
+
50
+ # Memory & Thermals Table
51
+ mem_table = Table(title="Memory Envelope & Thermals", show_header=True, header_style="bold magenta")
52
+ mem_table.add_column("Metric", style="bold white", width=22)
53
+ mem_table.add_column("Value", style="cyan")
54
+
55
+ mem_table.add_row("Physical RAM Total", f"{hw.ram_total_mb} MB")
56
+ mem_table.add_row("Physical RAM Available", f"[bold green]{hw.ram_avail_mb} MB[/bold green]")
57
+ mem_table.add_row("Swap Total / Free", f"{hw.swap_total_mb} MB / {hw.swap_free_mb} MB")
58
+
59
+ if hw.temp_c > 0:
60
+ temp_color = "green" if hw.temp_c < 75 else ("yellow" if hw.temp_c < 80 else "bold red")
61
+ mem_table.add_row("Thermal Zone 0", f"[{temp_color}]{hw.temp_c}°C ({gov.thermal_state})[/{temp_color}]")
62
+ else:
63
+ mem_table.add_row("Thermal Zone 0", "[dim]Unavailable[/dim]")
64
+
65
+ mem_table.add_row("Recommended Threads", f"{gov.recommended_threads} threads")
66
+
67
+ console.print(mem_table)
68
+
69
+ # Vector Extensions Table
70
+ vec_table = Table(title="Hardware ISA Vector Acceleration", show_header=True, header_style="bold magenta")
71
+ vec_table.add_column("Extension", style="bold white", width=22)
72
+ vec_table.add_column("Status", width=12)
73
+ vec_table.add_column("Impact on Small LLMs", style="dim")
74
+
75
+ def fmt_flag(val: bool, note: str):
76
+ status = "[bold green]YES[/bold green]" if val else "[red]NO[/red]"
77
+ return status, note
78
+
79
+ if hw.arch in ["aarch64", "arm64", "armv7l"]:
80
+ s, note = fmt_flag(hw.has_neon, "Baseline 128-bit SIMD vector engine")
81
+ vec_table.add_row("ARM NEON (ASIMD)", s, note)
82
+
83
+ s, note = fmt_flag(hw.has_dotprod, "sdot/udot INT8 acceleration (Pi 5 / RK3588)")
84
+ vec_table.add_row("Dot Product (ASIMDDP)", s, note)
85
+
86
+ s, note = fmt_flag(hw.has_fp16, "Native half-precision floating point ops")
87
+ vec_table.add_row("FP16 (FPHP)", s, note)
88
+
89
+ s, note = fmt_flag(hw.has_i8mm, "Int8 matrix multiplication extensions")
90
+ vec_table.add_row("I8MM", s, note)
91
+
92
+ s, note = fmt_flag(hw.has_sve, "Scalable Vector Extension")
93
+ vec_table.add_row("ARM SVE", s, note)
94
+ else:
95
+ s, note = fmt_flag(hw.has_avx, "Advanced Vector Extensions 256-bit")
96
+ vec_table.add_row("x86 AVX", s, note)
97
+
98
+ s, note = fmt_flag(hw.has_avx2, "Fused Integer Vector ops (crucial for quant speed)")
99
+ vec_table.add_row("x86 AVX2", s, note)
100
+
101
+ s, note = fmt_flag(hw.has_fma, "Fused Multiply-Add")
102
+ vec_table.add_row("x86 FMA", s, note)
103
+
104
+ s, note = fmt_flag(hw.has_avx512, "512-bit vector registers")
105
+ vec_table.add_row("x86 AVX-512", s, note)
106
+
107
+ if hw.has_rknn:
108
+ vec_table.add_row("Rockchip RKNN NPU", "[bold green]DETECTED[/bold green]", "/dev/rknpu hardware node accessible")
109
+ if hw.has_nvdla:
110
+ vec_table.add_row("NVIDIA NVDLA", "[bold green]DETECTED[/bold green]", "Jetson deep learning accelerator accessible")
111
+
112
+ console.print(vec_table)
113
+
litrun/cli/cmd_list.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ litrun list: Catalog of models and local cache status
3
+ """
4
+
5
+ import os
6
+ import click
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+ from litrun.registry import list_models
12
+ from litrun.resolver import get_models_cache_dir, get_bin_cache_dir
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.command(name="list")
18
+ def list_cmd():
19
+ """Lists curated edge LLMs, available quantizations, and local cache status."""
20
+ models = list_models()
21
+ cache_dir = get_models_cache_dir()
22
+
23
+ title = Text("⚡ litrun Model & Binary Registry", style="bold cyan")
24
+ console.print(Panel(title, expand=False))
25
+
26
+ table = Table(title="Curated Embedded AI Models", show_header=True, header_style="bold magenta")
27
+ table.add_column("Model ID", style="bold yellow", no_wrap=True)
28
+ table.add_column("Model Name", style="bold white")
29
+ table.add_column("Context", justify="right")
30
+ table.add_column("Quant", style="cyan")
31
+ table.add_column("Weight", justify="right")
32
+ table.add_column("RAM Min", justify="right")
33
+ table.add_column("Status")
34
+
35
+ for m in models:
36
+ rec = m.get_recommended_variant()
37
+ target_path = os.path.join(cache_dir, f"{m.id}-{rec.quant.lower()}.gguf".replace("/", "-"))
38
+ is_cached = os.path.isfile(target_path)
39
+ status_str = "[bold green]CACHED[/bold green]" if is_cached else "[dim]Available[/dim]"
40
+
41
+ table.add_row(
42
+ m.id,
43
+ m.name,
44
+ f"{m.default_context}",
45
+ rec.quant,
46
+ f"{rec.size_mb} MB",
47
+ f"{rec.ram_required_mb} MB",
48
+ status_str,
49
+ )
50
+
51
+ console.print(table)
52
+
53
+ # Cached Binaries
54
+ bin_dir = get_bin_cache_dir()
55
+ bin_files = os.listdir(bin_dir) if os.path.isdir(bin_dir) else []
56
+
57
+ if bin_files:
58
+ bin_table = Table(title="Cached Inference Engine Binaries", show_header=True, header_style="bold magenta")
59
+ bin_table.add_column("Engine Target", style="bold green")
60
+ bin_table.add_column("Path", style="dim")
61
+
62
+ for bf in bin_files:
63
+ p = os.path.join(bin_dir, bf)
64
+ bin_table.add_row(bf, p)
65
+
66
+ console.print(bin_table)
67
+ else:
68
+ console.print("[dim]No precompiled inference engine binaries cached yet. (They will auto-pull on first 'litrun run').[/dim]\n")
litrun/cli/cmd_pull.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ litrun pull: Pre-cache model weights locally
3
+ """
4
+
5
+ import sys
6
+ import click
7
+ from rich.console import Console
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn
9
+ from litrun.registry import get_model
10
+ from litrun.resolver import resolve_model_weights, get_model_cache_path
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command(name="pull")
16
+ @click.argument("model_id")
17
+ @click.option("-q", "--quant", "quant_name", default=None, help="Specific quantization to pull (e.g. Q4_K_M, Q8_0)")
18
+ @click.option("-f", "--force", is_flag=True, help="Force re-download even if already cached")
19
+ def pull_cmd(model_id: str, quant_name: str, force: bool):
20
+ """Downloads and cryptographically verifies GGUF model weights into local cache."""
21
+ entry = get_model(model_id)
22
+ if not entry:
23
+ console.print(f"[bold red]✗ Error:[/bold red] Model '{model_id}' not found in registry.")
24
+ sys.exit(1)
25
+
26
+ variant = entry.get_variant(quant_name) if quant_name else entry.get_recommended_variant()
27
+ if not variant:
28
+ console.print(f"[bold red]✗ Error:[/bold red] Quantization '{quant_name}' not available for {model_id}.")
29
+ sys.exit(1)
30
+
31
+ cached_path = get_model_cache_path(entry.id, variant.quant)
32
+ import os
33
+ if os.path.isfile(cached_path) and not force:
34
+ console.print(f"[bold green]✓[/bold green] Model [bold white]{entry.id}-{variant.quant}[/bold white] is already cached at [dim]{cached_path}[/dim]")
35
+ return
36
+
37
+ console.print(f"[bold blue]→ Streaming model weights:[/bold blue] {entry.name} ({variant.quant}, {variant.size_mb} MB)")
38
+ console.print(f"[dim]Source: {variant.url}[/dim]")
39
+
40
+ with Progress(
41
+ SpinnerColumn(),
42
+ TextColumn("[progress.description]{task.description}"),
43
+ BarColumn(),
44
+ DownloadColumn(),
45
+ TransferSpeedColumn(),
46
+ ) as progress:
47
+ task = progress.add_task(f"Pulling {entry.id}-{variant.quant}", total=variant.size_mb * 1024 * 1024)
48
+
49
+ def update_progress(done, total):
50
+ progress.update(task, completed=done, total=total)
51
+
52
+ try:
53
+ path = resolve_model_weights(
54
+ entry,
55
+ variant,
56
+ force_download=force,
57
+ progress_callback=update_progress,
58
+ )
59
+ console.print(f"[bold green]✓ Successfully downloaded and verified:[/bold green] {path}")
60
+ except Exception as e:
61
+ console.print(f"[bold red]✗ Download failed:[/bold red] {str(e)}")
62
+ sys.exit(1)
63
+
litrun/cli/cmd_run.py ADDED
@@ -0,0 +1,200 @@
1
+ """
2
+ litrun run: One-command hardware-aware model execution
3
+ """
4
+
5
+ import sys
6
+ import time
7
+ import click
8
+ from rich.console import Console
9
+ from rich.text import Text
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn
11
+ from litrun.hardware import probe_hardware
12
+ from litrun.registry import get_model, select_optimal_variant
13
+ from litrun.governor import check_ram_envelope, poll_governor
14
+ from litrun.resolver import (
15
+ select_binary_target,
16
+ get_model_cache_path,
17
+ resolve_engine_binary,
18
+ resolve_model_weights,
19
+ )
20
+ from litrun.engine import get_backend
21
+ from litrun.session import Session
22
+
23
+ console = Console()
24
+
25
+
26
+ @click.command(name="run")
27
+ @click.argument("model_id", default="qwen2.5-0.5b")
28
+ @click.option("-p", "--prompt", "prompt_text", default=None, help="Direct prompt to execute")
29
+ @click.option("-q", "--quant", "preferred_quant", default=None, help="Preferred quantization (e.g. Q4_K_M, Q8_0)")
30
+ @click.option("-t", "--threads", "thread_count", default=None, type=int, help="Thread count override")
31
+ @click.option("--system", "system_prompt", default=None, help="System prompt override")
32
+ @click.option("--demo", is_flag=True, help="Run in high-speed hardware simulation mode without downloading weights")
33
+ def run_cmd(model_id: str, prompt_text: str, preferred_quant: str, thread_count: int, system_prompt: str, demo: bool):
34
+ """Executes any curated LLM with zero quantization guesswork."""
35
+
36
+ # 1. Probing Hardware
37
+ with console.status("[bold cyan]Probing hardware topology & ISA extensions...[/bold cyan]", spinner="dots"):
38
+ hw = probe_hardware()
39
+ gov = poll_governor()
40
+
41
+ console.print(
42
+ f"[bold blue]→ Detected:[/bold blue] "
43
+ f"[bold white]{hw.board_name}[/bold white] "
44
+ f"({hw.soc} {hw.cpu_part}, {hw.ram_total_mb} MB RAM, {hw.ram_avail_mb} MB free)"
45
+ )
46
+
47
+ # 2. Model Registry Lookup & Smart Quantization Selection
48
+ entry = get_model(model_id)
49
+ if not entry:
50
+ console.print(f"[bold red]✗ Error:[/bold red] Model '[bold yellow]{model_id}[/bold yellow]' not found in registry.")
51
+ console.print("Run [bold cyan]litrun list[/bold cyan] to view available models.")
52
+ sys.exit(1)
53
+
54
+ variant, decision_msg = select_optimal_variant(entry, hw, preferred_quant=preferred_quant)
55
+
56
+ allowed, needed_mb, gov_reason, is_tight = check_ram_envelope(
57
+ variant.size_mb,
58
+ context_length=entry.default_context,
59
+ kv_bytes_per_token=entry.kv_bytes_per_token,
60
+ )
61
+
62
+ status_tag = "[bold green]optimal RAM envelope[/bold green]" if not is_tight else "[bold yellow]tight RAM envelope[/bold yellow]"
63
+ console.print(
64
+ f"[bold blue]→ Selected:[/bold blue] "
65
+ f"[bold white]{entry.id}-{variant.quant}[/bold white] "
66
+ f"({status_tag}: {needed_mb}MB required / {hw.ram_avail_mb}MB free)"
67
+ )
68
+
69
+ if not allowed:
70
+ console.print(f"[bold red]✗ Governor Warning:[/bold red] {gov_reason}")
71
+
72
+ # 3. Precompiled Binary Resolution
73
+ bin_target = select_binary_target(hw)
74
+ with console.status(f"[bold cyan]Resolving precompiled engine for {bin_target}...[/bold cyan]", spinner="dots"):
75
+ try:
76
+ bin_path = resolve_engine_binary(hw)
77
+ console.print(f"[bold blue]→ Binary:[/bold blue] tuned {bin_target} engine ready [green][cached][/green]")
78
+ except Exception as e:
79
+ bin_path = None
80
+ console.print(f"[bold blue]→ Binary:[/bold blue] using simulation engine ([dim]{e}[/dim])")
81
+
82
+ # 4. Model Weights Check or Stream Pull
83
+ model_path = get_model_cache_path(entry.id, variant.quant)
84
+ import os
85
+ if demo:
86
+ console.print(f"[bold blue]→ Model:[/bold blue] running in hardware simulator mode ([dim]demo mode[/dim])")
87
+ model_path = None
88
+ elif os.path.isfile(model_path):
89
+ console.print(f"[bold blue]→ Model:[/bold blue] {entry.id}-{variant.quant} weights ready [green][cached][/green]")
90
+ else:
91
+ console.print(f"[bold blue]→ Model:[/bold blue] streaming weights from HuggingFace ({variant.size_mb}MB)...")
92
+ try:
93
+ with Progress(
94
+ SpinnerColumn(),
95
+ TextColumn("[progress.description]{task.description}"),
96
+ BarColumn(),
97
+ DownloadColumn(),
98
+ TransferSpeedColumn(),
99
+ ) as progress:
100
+ task = progress.add_task(f"Downloading {entry.id} ({variant.quant})", total=variant.size_mb * 1024 * 1024)
101
+
102
+ def update_cb(done, total):
103
+ progress.update(task, completed=done, total=total)
104
+
105
+ model_path = resolve_model_weights(entry, variant, progress_callback=update_cb)
106
+ console.print(f"[bold green]✓ Download verified with SHA-256[/bold green]")
107
+ except Exception as e:
108
+ console.print(f"[yellow]! Offline mode / demo simulation enabled ([dim]{e}[/dim])[/yellow]")
109
+ model_path = None
110
+
111
+ # 5. Dynamic Concurrency & Warmup
112
+ final_threads = thread_count or gov.recommended_threads
113
+ backend = get_backend(
114
+ binary_path=bin_path,
115
+ model_path=model_path,
116
+ threads=final_threads,
117
+ context_length=entry.default_context,
118
+ model_id=entry.id,
119
+ allow_mock=True,
120
+ )
121
+
122
+ expected_speed = 14.2
123
+ for bp in entry.hardware_profiles:
124
+ if hw.profile_id.startswith(bp.profile.split("-")[0]):
125
+ expected_speed = bp.tokens_per_sec
126
+ break
127
+
128
+ console.print(
129
+ f"[bold blue]→ Warmup complete.[/bold blue] "
130
+ f"Ready at ~{expected_speed:.1f} tokens/sec on {final_threads} threads. Type to interact."
131
+ )
132
+
133
+ # 6. Execution Loop
134
+ try:
135
+ session = Session(max_context_tokens=entry.default_context)
136
+ if system_prompt:
137
+ session.system_prompt = system_prompt
138
+
139
+ if prompt_text:
140
+ console.print(f"\n[bold green]>[/bold green] {prompt_text}\n")
141
+ session.add_user_message(prompt_text)
142
+ formatted = session.format_prompt(family=entry.family)
143
+
144
+ console.print(f"[bold cyan]litrun:[/bold cyan] ", end="")
145
+ chunks = []
146
+ t0 = time.perf_counter()
147
+ token_count = 0
148
+
149
+ for chunk in backend.stream(formatted, threads=final_threads):
150
+ sys.stdout.write(chunk)
151
+ sys.stdout.flush()
152
+ chunks.append(chunk)
153
+ token_count += 1
154
+
155
+ t1 = time.perf_counter()
156
+ elapsed = t1 - t0
157
+ actual_tps = (token_count / elapsed) if elapsed > 0.05 else expected_speed
158
+
159
+ console.print(f"\n\n[dim]⚡ Telemetry: {token_count} tokens generated in {elapsed:.2f}s ({actual_tps:.1f} tok/s)[/dim]\n")
160
+ return
161
+
162
+ # Interactive REPL
163
+ console.print("[dim]Type your prompt below. Enter 'exit' to quit, 'clear' to reset context.[/dim]\n")
164
+ while True:
165
+ try:
166
+ user_msg = console.input("[bold green]>[/bold green] ").strip()
167
+ if not user_msg:
168
+ continue
169
+ if user_msg.lower() in ["exit", "quit"]:
170
+ break
171
+ if user_msg.lower() == "clear":
172
+ session.clear()
173
+ console.print("[dim]Context memory cleared.[/dim]\n")
174
+ continue
175
+
176
+ session.add_user_message(user_msg)
177
+ formatted = session.format_prompt(family=entry.family)
178
+
179
+ console.print(f"[bold cyan]litrun:[/bold cyan] ", end="")
180
+ chunks = []
181
+ t0 = time.perf_counter()
182
+ token_count = 0
183
+
184
+ for chunk in backend.stream(formatted, threads=final_threads):
185
+ sys.stdout.write(chunk)
186
+ sys.stdout.flush()
187
+ chunks.append(chunk)
188
+ token_count += 1
189
+
190
+ t1 = time.perf_counter()
191
+ elapsed = t1 - t0
192
+ actual_tps = (token_count / elapsed) if elapsed > 0.05 else expected_speed
193
+
194
+ session.add_assistant_message("".join(chunks))
195
+ console.print(f"\n[dim]({actual_tps:.1f} tok/s)[/dim]\n")
196
+ except (KeyboardInterrupt, EOFError):
197
+ console.print("\n[dim]Session terminated.[/dim]")
198
+ break
199
+ finally:
200
+ backend.close()
@@ -0,0 +1,19 @@
1
+ """
2
+ litrun serve: Start OpenAI-compatible HTTP REST server
3
+ """
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from litrun.server import start_server
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command(name="serve")
13
+ @click.argument("model_id", default="qwen2.5-0.5b")
14
+ @click.option("--host", default="0.0.0.0", help="Network host interface to bind")
15
+ @click.option("--port", default=8080, type=int, help="Port to listen on (default: 8080)")
16
+ def serve_cmd(model_id: str, host: str, port: int):
17
+ """Starts an OpenAI-compatible REST server (POST /v1/chat/completions)."""
18
+ start_server(host=host, port=port, model_id=model_id)
19
+
litrun/cli/main.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ litrun CLI Entrypoint
3
+ """
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from litrun import __version__
8
+ from litrun.cli.cmd_inspect import inspect_cmd
9
+ from litrun.cli.cmd_run import run_cmd
10
+ from litrun.cli.cmd_pull import pull_cmd
11
+ from litrun.cli.cmd_list import list_cmd
12
+ from litrun.cli.cmd_serve import serve_cmd
13
+ from litrun.cli.cmd_bench import bench_cmd
14
+
15
+ console = Console()
16
+
17
+
18
+ @click.group()
19
+ @click.version_option(version=__version__, prog_name="litrun")
20
+ def cli():
21
+ """⚡ litrun: Universal Lightweight Embedded Linux AI Runtime"""
22
+ pass
23
+
24
+
25
+ cli.add_command(inspect_cmd, name="inspect")
26
+ cli.add_command(run_cmd, name="run")
27
+ cli.add_command(pull_cmd, name="pull")
28
+ cli.add_command(list_cmd, name="list")
29
+ cli.add_command(serve_cmd, name="serve")
30
+ cli.add_command(bench_cmd, name="benchmark")
31
+
32
+ if __name__ == "__main__":
33
+ cli()
34
+