hypercli-cli 0.4.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.
c3cli/instances.py ADDED
@@ -0,0 +1,193 @@
1
+ """c3 instances commands"""
2
+ import typer
3
+ from typing import Optional
4
+ from c3 import C3
5
+ from .output import output, console, success, spinner
6
+
7
+ app = typer.Typer(help="GPU instances - browse and launch")
8
+
9
+
10
+ def get_client() -> C3:
11
+ return C3()
12
+
13
+
14
+ @app.command("list")
15
+ def list_instances(
16
+ gpu: Optional[str] = typer.Option(None, "--gpu", "-g", help="Filter by GPU type"),
17
+ region: Optional[str] = typer.Option(None, "--region", "-r", help="Filter by region"),
18
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
19
+ ):
20
+ """List available GPU instances with pricing"""
21
+ c3 = get_client()
22
+ with spinner("Fetching instances..."):
23
+ available = c3.instances.list_available(gpu_type=gpu, region=region)
24
+
25
+ if fmt == "json":
26
+ output(available, "json")
27
+ else:
28
+ from rich.table import Table
29
+ table = Table(show_header=True, header_style="bold cyan")
30
+ table.add_column("GPU")
31
+ table.add_column("Count", justify="right")
32
+ table.add_column("Region")
33
+ table.add_column("Spot $/hr", justify="right")
34
+ table.add_column("On-Demand $/hr", justify="right")
35
+ table.add_column("vCPUs", justify="right")
36
+ table.add_column("RAM GB", justify="right")
37
+
38
+ for item in sorted(available, key=lambda x: (x["gpu_type"], x["gpu_count"], x.get("price_spot") or 999)):
39
+ spot_price = f"${item['price_spot']:.2f}" if item['price_spot'] else "-"
40
+ od_price = f"${item['price_on_demand']:.2f}" if item['price_on_demand'] else "-"
41
+
42
+ table.add_row(
43
+ f"[green]{item['gpu_type']}[/]",
44
+ str(item['gpu_count']),
45
+ f"{item['region']} ({item['region_name']})",
46
+ f"[cyan]{spot_price}[/]",
47
+ od_price,
48
+ str(int(item['cpu_cores'])),
49
+ str(int(item['memory_gb'])),
50
+ )
51
+
52
+ console.print(table)
53
+
54
+
55
+ @app.command("gpus")
56
+ def list_gpus(
57
+ region: Optional[str] = typer.Option(None, "--region", "-r", help="Filter by region"),
58
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
59
+ ):
60
+ """List available GPU types"""
61
+ c3 = get_client()
62
+ with spinner("Fetching GPU types..."):
63
+ types = c3.instances.types()
64
+
65
+ if fmt == "json":
66
+ output({k: {"name": v.name, "description": v.description, "configs": [
67
+ {"gpu_count": c.gpu_count, "cpu_cores": c.cpu_cores, "memory_gb": c.memory_gb, "regions": c.regions}
68
+ for c in v.configs
69
+ ]} for k, v in types.items()}, "json")
70
+ else:
71
+ from rich.table import Table
72
+ table = Table(show_header=True, header_style="bold cyan")
73
+ table.add_column("GPU Type")
74
+ table.add_column("Name")
75
+ table.add_column("Description")
76
+ table.add_column("Counts")
77
+ table.add_column("Regions")
78
+
79
+ for gpu_id, gpu in types.items():
80
+ available_counts = []
81
+ available_regions = set()
82
+ for config in gpu.configs:
83
+ if config.regions:
84
+ if region and region not in config.regions:
85
+ continue
86
+ available_counts.append(str(config.gpu_count))
87
+ available_regions.update(config.regions)
88
+
89
+ if not available_counts:
90
+ continue
91
+
92
+ table.add_row(
93
+ f"[green]{gpu_id}[/]",
94
+ gpu.name,
95
+ gpu.description,
96
+ ", ".join(available_counts),
97
+ ", ".join(sorted(available_regions)),
98
+ )
99
+
100
+ console.print(table)
101
+
102
+
103
+ @app.command("regions")
104
+ def list_regions(
105
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
106
+ ):
107
+ """List available regions"""
108
+ c3 = get_client()
109
+ with spinner("Fetching regions..."):
110
+ regions = c3.instances.regions()
111
+
112
+ if fmt == "json":
113
+ output({k: {"description": v.description, "country": v.country} for k, v in regions.items()}, "json")
114
+ else:
115
+ from rich.table import Table
116
+ table = Table(show_header=True, header_style="bold cyan")
117
+ table.add_column("Code")
118
+ table.add_column("Location")
119
+ table.add_column("Country")
120
+
121
+ for region_id, region in regions.items():
122
+ table.add_row(
123
+ f"[green]{region_id}[/]",
124
+ region.description,
125
+ region.country,
126
+ )
127
+
128
+ console.print(table)
129
+
130
+
131
+ @app.command("launch")
132
+ def launch(
133
+ image: str = typer.Argument(..., help="Docker image"),
134
+ command: Optional[str] = typer.Option(None, "--command", "-c", help="Command to run"),
135
+ gpu: str = typer.Option("l40s", "--gpu", "-g", help="GPU type"),
136
+ count: int = typer.Option(1, "--count", "-n", help="Number of GPUs"),
137
+ region: Optional[str] = typer.Option(None, "--region", "-r", help="Region code"),
138
+ runtime: Optional[int] = typer.Option(None, "--runtime", "-t", help="Runtime in seconds"),
139
+ interruptible: bool = typer.Option(True, "--interruptible/--on-demand", help="Use interruptible instances"),
140
+ env: Optional[list[str]] = typer.Option(None, "--env", "-e", help="Env vars (KEY=VALUE)"),
141
+ port: Optional[list[str]] = typer.Option(None, "--port", "-p", help="Ports (name:port)"),
142
+ follow: bool = typer.Option(False, "--follow", "-f", help="Follow logs after creation"),
143
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
144
+ ):
145
+ """Launch a new GPU instance"""
146
+ c3 = get_client()
147
+
148
+ # Parse env vars
149
+ env_dict = None
150
+ if env:
151
+ env_dict = {}
152
+ for e in env:
153
+ if "=" in e:
154
+ k, v = e.split("=", 1)
155
+ env_dict[k] = v
156
+
157
+ # Parse ports
158
+ ports_dict = None
159
+ if port:
160
+ ports_dict = {}
161
+ for p in port:
162
+ if ":" in p:
163
+ name, port_num = p.split(":", 1)
164
+ ports_dict[name] = int(port_num)
165
+
166
+ with spinner("Launching instance..."):
167
+ job = c3.jobs.create(
168
+ image=image,
169
+ command=command,
170
+ gpu_type=gpu,
171
+ gpu_count=count,
172
+ region=region,
173
+ runtime=runtime,
174
+ interruptible=interruptible,
175
+ env=env_dict,
176
+ ports=ports_dict,
177
+ )
178
+
179
+ if fmt == "json":
180
+ output(job, "json")
181
+ else:
182
+ success(f"Instance launched: {job.job_id}")
183
+ console.print(f" State: {job.state}")
184
+ console.print(f" GPU: {job.gpu_type} x{job.gpu_count}")
185
+ console.print(f" Region: {job.region}")
186
+ console.print(f" Price: ${job.price_per_hour:.2f}/hr")
187
+ if job.hostname:
188
+ console.print(f" Hostname: {job.hostname}")
189
+
190
+ if follow:
191
+ console.print()
192
+ from .tui.job_monitor import run_job_monitor
193
+ run_job_monitor(job.job_id)
c3cli/jobs.py ADDED
@@ -0,0 +1,239 @@
1
+ """c3 jobs commands"""
2
+ import typer
3
+ from typing import Optional
4
+ from c3 import C3
5
+ from .output import output, console, success, spinner
6
+
7
+ app = typer.Typer(help="Manage running jobs")
8
+
9
+
10
+ def get_client() -> C3:
11
+ return C3()
12
+
13
+
14
+ @app.command("list")
15
+ def list_jobs(
16
+ state: Optional[str] = typer.Option(None, "--state", "-s", help="Filter by state"),
17
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
18
+ ):
19
+ """List all jobs"""
20
+ c3 = get_client()
21
+ with spinner("Fetching jobs..."):
22
+ jobs = c3.jobs.list(state=state)
23
+
24
+ if fmt == "json":
25
+ output(jobs, "json")
26
+ else:
27
+ if not jobs:
28
+ console.print("[dim]No jobs found[/dim]")
29
+ return
30
+ output(jobs, "table", ["job_id", "state", "gpu_type", "gpu_count", "region", "hostname"])
31
+
32
+
33
+ @app.command("get")
34
+ def get_job(
35
+ job_id: str = typer.Argument(..., help="Job ID"),
36
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
37
+ ):
38
+ """Get job details"""
39
+ c3 = get_client()
40
+ with spinner("Fetching job..."):
41
+ job = c3.jobs.get(job_id)
42
+ output(job, fmt)
43
+
44
+
45
+ @app.command("logs")
46
+ def logs(
47
+ job_id: str = typer.Argument(..., help="Job ID"),
48
+ follow: bool = typer.Option(False, "--follow", "-f", help="Stream logs"),
49
+ ):
50
+ """Get job logs"""
51
+ c3 = get_client()
52
+
53
+ if follow:
54
+ _follow_job(job_id)
55
+ else:
56
+ with spinner("Fetching logs..."):
57
+ logs_str = c3.jobs.logs(job_id)
58
+ console.print(logs_str)
59
+
60
+
61
+ @app.command("metrics")
62
+ def metrics(
63
+ job_id: str = typer.Argument(..., help="Job ID"),
64
+ watch: bool = typer.Option(False, "--watch", "-w", help="Watch metrics live"),
65
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
66
+ ):
67
+ """Get job GPU metrics"""
68
+ c3 = get_client()
69
+
70
+ if watch:
71
+ _watch_metrics(job_id)
72
+ else:
73
+ with spinner("Fetching metrics..."):
74
+ m = c3.jobs.metrics(job_id)
75
+ if fmt == "json":
76
+ output(m, "json")
77
+ else:
78
+ _print_metrics(m)
79
+
80
+
81
+ @app.command("cancel")
82
+ def cancel(
83
+ job_id: str = typer.Argument(..., help="Job ID"),
84
+ ):
85
+ """Cancel a running job"""
86
+ c3 = get_client()
87
+ with spinner("Cancelling job..."):
88
+ c3.jobs.cancel(job_id)
89
+ success(f"Job {job_id} cancelled")
90
+
91
+
92
+ @app.command("extend")
93
+ def extend(
94
+ job_id: str = typer.Argument(..., help="Job ID"),
95
+ runtime: int = typer.Argument(..., help="New runtime in seconds"),
96
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
97
+ ):
98
+ """Extend job runtime"""
99
+ c3 = get_client()
100
+ with spinner("Extending runtime..."):
101
+ job = c3.jobs.extend(job_id, runtime)
102
+ if fmt == "json":
103
+ output(job, "json")
104
+ else:
105
+ success(f"Job extended to {runtime}s runtime")
106
+
107
+
108
+ def _print_metrics(m):
109
+ """Print GPU metrics"""
110
+ from rich.panel import Panel
111
+ from rich.table import Table
112
+
113
+ # System metrics (CPU/RAM)
114
+ if m.system:
115
+ sys_table = Table(show_header=False, box=None, padding=(0, 2))
116
+ sys_table.add_column("Metric", style="cyan")
117
+ sys_table.add_column("Value")
118
+ sys_table.add_column("Bar", width=30)
119
+
120
+ cpu_bar = _make_bar(m.system.cpu_percent, 100)
121
+ mem_pct = (m.system.memory_used / m.system.memory_limit * 100) if m.system.memory_limit else 0
122
+ mem_bar = _make_bar(mem_pct, 100)
123
+
124
+ sys_table.add_row("CPU", f"{m.system.cpu_percent:5.1f}%", cpu_bar)
125
+ sys_table.add_row("RAM", f"{m.system.memory_used/1024:.1f}/{m.system.memory_limit/1024:.1f} GB", mem_bar)
126
+
127
+ console.print(Panel(sys_table, title="[bold]System[/bold]"))
128
+
129
+ if not m.gpus:
130
+ console.print("[dim]No GPU metrics available[/dim]")
131
+ return
132
+
133
+ for gpu in m.gpus:
134
+ table = Table(show_header=False, box=None, padding=(0, 2))
135
+ table.add_column("Metric", style="cyan")
136
+ table.add_column("Value")
137
+ table.add_column("Bar", width=30)
138
+
139
+ util_bar = _make_bar(gpu.utilization, 100)
140
+ mem_pct = (gpu.memory_used / gpu.memory_total * 100) if gpu.memory_total else 0
141
+ mem_bar = _make_bar(mem_pct, 100)
142
+ temp_bar = _make_bar(gpu.temperature, 100, warn=70, crit=85)
143
+
144
+ table.add_row("GPU", f"{gpu.utilization:5.1f}%", util_bar)
145
+ table.add_row("VRAM", f"{gpu.memory_used/1024:.1f}/{gpu.memory_total/1024:.1f} GB", mem_bar)
146
+ table.add_row("Temp", f"{gpu.temperature}°C", temp_bar)
147
+ table.add_row("Power", f"{gpu.power_draw:.0f}W", "")
148
+
149
+ title = f"[bold]GPU {gpu.index}: {gpu.name}[/bold]" if gpu.name else f"[bold]GPU {gpu.index}[/bold]"
150
+ console.print(Panel(table, title=title))
151
+
152
+
153
+ def _make_bar(value: float, max_val: float, warn: float = None, crit: float = None) -> str:
154
+ """Create a colored progress bar"""
155
+ pct = min(value / max_val, 1.0) if max_val else 0
156
+ width = 25
157
+ filled = int(pct * width)
158
+
159
+ if crit and value >= crit:
160
+ color = "red"
161
+ elif warn and value >= warn:
162
+ color = "yellow"
163
+ else:
164
+ color = "green"
165
+
166
+ bar = "█" * filled + "░" * (width - filled)
167
+ return f"[{color}]{bar}[/{color}]"
168
+
169
+
170
+ def _follow_job(job_id: str):
171
+ """Follow job with TUI"""
172
+ from .tui.job_monitor import run_job_monitor
173
+ run_job_monitor(job_id)
174
+
175
+
176
+ def _watch_metrics(job_id: str):
177
+ """Watch metrics live"""
178
+ import time
179
+ from rich.live import Live
180
+
181
+ c3 = get_client()
182
+
183
+ with Live(console=console, refresh_per_second=2) as live:
184
+ while True:
185
+ try:
186
+ m = c3.jobs.metrics(job_id)
187
+ live.update(_render_metrics(m))
188
+ time.sleep(1)
189
+ except KeyboardInterrupt:
190
+ break
191
+ except Exception as e:
192
+ console.print(f"[red]Error: {e}[/red]")
193
+ break
194
+
195
+
196
+ def _render_metrics(m):
197
+ """Render metrics as Rich panel"""
198
+ from rich.panel import Panel
199
+ from rich.table import Table
200
+ from rich.console import Group
201
+
202
+ panels = []
203
+
204
+ # System metrics
205
+ if m.system:
206
+ sys_table = Table(show_header=False, box=None)
207
+ sys_table.add_column("Metric", style="cyan")
208
+ sys_table.add_column("Value")
209
+ cpu_bar = _make_bar(m.system.cpu_percent, 100)
210
+ mem_pct = (m.system.memory_used / m.system.memory_limit * 100) if m.system.memory_limit else 0
211
+ mem_bar = _make_bar(mem_pct, 100)
212
+ sys_table.add_row("CPU", f"{m.system.cpu_percent:5.1f}% {cpu_bar}")
213
+ sys_table.add_row("RAM", f"{m.system.memory_used/1024:.1f}/{m.system.memory_limit/1024:.1f}GB {mem_bar}")
214
+ panels.append(Panel(sys_table, title="[bold]System[/bold]", border_style="blue"))
215
+
216
+ if not m.gpus:
217
+ panels.append(Panel("[dim]No GPU metrics[/dim]"))
218
+ return Group(*panels)
219
+
220
+ table = Table(show_header=True, header_style="bold cyan", box=None)
221
+ table.add_column("GPU")
222
+ table.add_column("Util")
223
+ table.add_column("VRAM")
224
+ table.add_column("Temp")
225
+ table.add_column("Power")
226
+
227
+ for gpu in m.gpus:
228
+ util_bar = _make_bar(gpu.utilization, 100)
229
+ name = f"{gpu.index}: {gpu.name}" if gpu.name else str(gpu.index)
230
+ table.add_row(
231
+ f"[bold]{name}[/bold]",
232
+ f"{gpu.utilization:5.1f}% {util_bar}",
233
+ f"{gpu.memory_used/1024:.1f}/{gpu.memory_total/1024:.1f}GB",
234
+ f"{gpu.temperature}°C",
235
+ f"{gpu.power_draw:.0f}W"
236
+ )
237
+
238
+ panels.append(Panel(table, title="[bold]GPU Metrics[/bold]", border_style="green"))
239
+ return Group(*panels)
c3cli/llm.py ADDED
@@ -0,0 +1,263 @@
1
+ """c3 llm commands - uses OpenAI SDK"""
2
+ import typer
3
+ from typing import Optional
4
+ from openai import OpenAI
5
+ from c3.config import get_api_key, get_api_url
6
+ from .output import output, console, spinner
7
+
8
+ app = typer.Typer(help="LLM API commands")
9
+
10
+
11
+ def get_openai_client() -> OpenAI:
12
+ """Get OpenAI client configured for C3"""
13
+ api_key = get_api_key()
14
+ if not api_key:
15
+ raise typer.Exit("C3_API_KEY not set. Run: c3 configure")
16
+
17
+ base_url = get_api_url()
18
+ return OpenAI(api_key=api_key, base_url=f"{base_url}/v1")
19
+
20
+
21
+ @app.command("models")
22
+ def models(
23
+ fmt: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
24
+ ):
25
+ """List available models"""
26
+ client = get_openai_client()
27
+ with spinner("Fetching models..."):
28
+ models_list = list(client.models.list())
29
+
30
+ if fmt == "json":
31
+ output([{"id": m.id, "owned_by": m.owned_by} for m in models_list], "json")
32
+ else:
33
+ data = [{"id": m.id, "owned_by": m.owned_by} for m in models_list]
34
+ output(data, "table", ["id", "owned_by"])
35
+
36
+
37
+ @app.command("chat")
38
+ def chat(
39
+ model: Optional[str] = typer.Argument(None, help="Model name (optional for interactive)"),
40
+ prompt: Optional[str] = typer.Argument(None, help="Prompt (omit for interactive)"),
41
+ system: Optional[str] = typer.Option(None, "--system", "-s", help="System message"),
42
+ max_tokens: int = typer.Option(4096, "--max-tokens", "-m", help="Max tokens"),
43
+ temperature: float = typer.Option(0.7, "--temperature", "-t", help="Temperature"),
44
+ no_stream: bool = typer.Option(False, "--no-stream", help="Disable streaming"),
45
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
46
+ fmt: str = typer.Option("text", "--output", "-o", help="Output format: text|json"),
47
+ ):
48
+ """Chat completion"""
49
+ client = get_openai_client()
50
+
51
+ # Interactive mode if -i flag or no model provided
52
+ if interactive or model is None:
53
+ _interactive_chat(client, model, system, max_tokens, temperature)
54
+ return
55
+
56
+ if prompt is None:
57
+ _interactive_chat(client, model, system, max_tokens, temperature)
58
+ return
59
+
60
+ messages = []
61
+ if system:
62
+ messages.append({"role": "system", "content": system})
63
+ messages.append({"role": "user", "content": prompt})
64
+
65
+ if no_stream or fmt == "json":
66
+ with spinner("Generating response..."):
67
+ response = client.chat.completions.create(
68
+ model=model,
69
+ messages=messages,
70
+ max_tokens=max_tokens,
71
+ temperature=temperature,
72
+ )
73
+ if fmt == "json":
74
+ output(response.model_dump(), "json")
75
+ else:
76
+ console.print(response.choices[0].message.content)
77
+ else:
78
+ stream = client.chat.completions.create(
79
+ model=model,
80
+ messages=messages,
81
+ max_tokens=max_tokens,
82
+ temperature=temperature,
83
+ stream=True,
84
+ )
85
+ for chunk in stream:
86
+ content = chunk.choices[0].delta.content
87
+ if content:
88
+ console.print(content, end="")
89
+ console.print()
90
+
91
+
92
+ def _interactive_chat(client: OpenAI, model: Optional[str], system: Optional[str], max_tokens: int, temperature: float):
93
+ """Interactive chat mode with slash commands"""
94
+ from rich.table import Table
95
+
96
+ # Default model if none provided
97
+ current_model = model or "meta-llama/llama-3.3-70b-instruct"
98
+ current_system = system
99
+ current_temp = temperature
100
+ current_max_tokens = max_tokens
101
+ messages: list[dict] = []
102
+
103
+ if current_system:
104
+ messages.append({"role": "system", "content": current_system})
105
+
106
+ def show_help():
107
+ console.print("\n[bold cyan]Commands:[/bold cyan]")
108
+ table = Table(show_header=False, box=None, padding=(0, 2))
109
+ table.add_column("Command", style="green")
110
+ table.add_column("Description")
111
+ table.add_row("/model <name>", "Switch to a different model")
112
+ table.add_row("/models", "List available models")
113
+ table.add_row("/system <prompt>", "Set system prompt")
114
+ table.add_row("/temp <value>", "Set temperature (0.0-2.0)")
115
+ table.add_row("/tokens <value>", "Set max tokens")
116
+ table.add_row("/clear", "Clear conversation history")
117
+ table.add_row("/history", "Show conversation history")
118
+ table.add_row("/help", "Show this help")
119
+ table.add_row("/quit", "Exit chat")
120
+ console.print(table)
121
+ console.print()
122
+
123
+ def show_status():
124
+ console.print(f"[dim]Model: [green]{current_model}[/] | Temp: {current_temp} | Max tokens: {current_max_tokens}[/]")
125
+ if current_system:
126
+ sys_preview = current_system[:50] + "..." if len(current_system) > 50 else current_system
127
+ console.print(f"[dim]System: {sys_preview}[/]")
128
+
129
+ console.print("\n[bold cyan]C3 Chat[/bold cyan]")
130
+ show_status()
131
+ console.print("[dim]Type /help for commands, /quit to exit[/dim]\n")
132
+
133
+ while True:
134
+ try:
135
+ user_input = console.input("[bold blue]> [/bold blue]").strip()
136
+
137
+ if not user_input:
138
+ continue
139
+
140
+ # Handle slash commands
141
+ if user_input.startswith("/"):
142
+ parts = user_input[1:].split(maxsplit=1)
143
+ cmd = parts[0].lower()
144
+ arg = parts[1] if len(parts) > 1 else None
145
+
146
+ if cmd in ("quit", "exit", "q"):
147
+ console.print("[dim]Goodbye![/dim]")
148
+ break
149
+
150
+ elif cmd == "help":
151
+ show_help()
152
+
153
+ elif cmd == "models":
154
+ with spinner("Fetching models..."):
155
+ models_list = list(client.models.list())
156
+ console.print("\n[bold]Available models:[/bold]")
157
+ for m in models_list:
158
+ marker = "[green]●[/]" if m.id == current_model else "[dim]○[/]"
159
+ console.print(f" {marker} {m.id}")
160
+ console.print()
161
+
162
+ elif cmd == "model":
163
+ if not arg:
164
+ console.print(f"[dim]Current model: [green]{current_model}[/][/]")
165
+ else:
166
+ current_model = arg
167
+ console.print(f"[green]✓[/] Switched to model: [bold]{current_model}[/]")
168
+
169
+ elif cmd == "system":
170
+ if not arg:
171
+ if current_system:
172
+ console.print(f"[dim]System prompt: {current_system}[/]")
173
+ else:
174
+ console.print("[dim]No system prompt set[/]")
175
+ else:
176
+ current_system = arg
177
+ # Update or add system message
178
+ if messages and messages[0]["role"] == "system":
179
+ messages[0]["content"] = current_system
180
+ else:
181
+ messages.insert(0, {"role": "system", "content": current_system})
182
+ console.print(f"[green]✓[/] System prompt updated")
183
+
184
+ elif cmd == "temp":
185
+ if not arg:
186
+ console.print(f"[dim]Temperature: {current_temp}[/]")
187
+ else:
188
+ try:
189
+ current_temp = float(arg)
190
+ console.print(f"[green]✓[/] Temperature set to {current_temp}")
191
+ except ValueError:
192
+ console.print("[red]Invalid temperature value[/]")
193
+
194
+ elif cmd == "tokens":
195
+ if not arg:
196
+ console.print(f"[dim]Max tokens: {current_max_tokens}[/]")
197
+ else:
198
+ try:
199
+ current_max_tokens = int(arg)
200
+ console.print(f"[green]✓[/] Max tokens set to {current_max_tokens}")
201
+ except ValueError:
202
+ console.print("[red]Invalid token value[/]")
203
+
204
+ elif cmd == "clear":
205
+ messages.clear()
206
+ if current_system:
207
+ messages.append({"role": "system", "content": current_system})
208
+ console.print("[green]✓[/] Conversation cleared")
209
+
210
+ elif cmd == "history":
211
+ if not messages or (len(messages) == 1 and messages[0]["role"] == "system"):
212
+ console.print("[dim]No conversation history[/]")
213
+ else:
214
+ console.print("\n[bold]Conversation history:[/bold]")
215
+ for msg in messages:
216
+ if msg["role"] == "system":
217
+ console.print(f"[dim]System: {msg['content'][:100]}...[/]")
218
+ elif msg["role"] == "user":
219
+ console.print(f"[blue]You:[/] {msg['content'][:100]}...")
220
+ else:
221
+ console.print(f"[green]Assistant:[/] {msg['content'][:100]}...")
222
+ console.print()
223
+
224
+ else:
225
+ console.print(f"[red]Unknown command: /{cmd}[/] - type /help for commands")
226
+
227
+ continue
228
+
229
+ # Regular message - send to model
230
+ messages.append({"role": "user", "content": user_input})
231
+
232
+ console.print("[bold green]Assistant:[/bold green] ", end="")
233
+
234
+ try:
235
+ full_response = []
236
+ stream = client.chat.completions.create(
237
+ model=current_model,
238
+ messages=messages,
239
+ max_tokens=current_max_tokens,
240
+ temperature=current_temp,
241
+ stream=True,
242
+ )
243
+
244
+ for chunk in stream:
245
+ if chunk.choices and chunk.choices[0].delta.content:
246
+ content = chunk.choices[0].delta.content
247
+ console.print(content, end="")
248
+ full_response.append(content)
249
+
250
+ console.print("\n")
251
+ messages.append({"role": "assistant", "content": "".join(full_response)})
252
+
253
+ except Exception as e:
254
+ console.print(f"\n[red]Error: {e}[/]\n")
255
+ # Remove the failed user message
256
+ messages.pop()
257
+
258
+ except KeyboardInterrupt:
259
+ console.print("\n[dim]Goodbye![/dim]")
260
+ break
261
+ except EOFError:
262
+ console.print("\n[dim]Goodbye![/dim]")
263
+ break