lyceum-cli 1.0.14__py3-none-any.whl → 1.0.18__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.
@@ -0,0 +1,315 @@
1
+ """
2
+ OpenAI-compatible batch processing commands
3
+ """
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import typer
10
+ import httpx
11
+ from rich.console import Console
12
+
13
+ from ....shared.config import config
14
+ from ....shared.display import create_table, format_timestamp, truncate_id
15
+
16
+ console = Console()
17
+
18
+ batch_app = typer.Typer(name="batch", help="OpenAI-compatible batch processing")
19
+
20
+
21
+ @batch_app.command("upload")
22
+ def upload_file(
23
+ file_path: str = typer.Argument(..., help="Path to JSONL file to upload"),
24
+ purpose: str = typer.Option("batch", "--purpose", "-p", help="File purpose (batch, batch_output, batch_errors)"),
25
+ ):
26
+ """Upload a JSONL file for batch processing"""
27
+ # Check if file exists
28
+ if not Path(file_path).exists():
29
+ console.print(f"[red]Error: File '{file_path}' not found[/red]")
30
+ raise typer.Exit(1)
31
+
32
+ # Validate file extension
33
+ if not file_path.endswith('.jsonl'):
34
+ console.print("[yellow]Warning: File doesn't have .jsonl extension[/yellow]")
35
+
36
+ try:
37
+ console.print(f"[dim]📤 Uploading {os.path.basename(file_path)} for {purpose}...[/dim]")
38
+
39
+ # Upload file using multipart form data
40
+ with open(file_path, 'rb') as f:
41
+ files = {'file': (os.path.basename(file_path), f, 'application/jsonl')}
42
+ data = {'purpose': purpose}
43
+
44
+ response = httpx.post(
45
+ f"{config.base_url}/api/v2/external/files",
46
+ headers={"Authorization": f"Bearer {config.api_key}"},
47
+ files=files,
48
+ data=data,
49
+ timeout=60.0
50
+ )
51
+
52
+ if response.status_code != 200:
53
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
54
+ console.print(f"[red]{response.text}[/red]")
55
+ raise typer.Exit(1)
56
+
57
+ data = response.json()
58
+
59
+ console.print(f"[green]✅ File uploaded successfully![/green]")
60
+ console.print(f"[cyan]File ID: {data['id']}[/cyan]")
61
+ console.print(f"[dim]Size: {data['bytes']} bytes[/dim]")
62
+ console.print(f"[dim]Purpose: {data['purpose']}[/dim]")
63
+ console.print(f"[dim]Created: {data['created_at']}[/dim]")
64
+
65
+ except Exception as e:
66
+ console.print(f"[red]Error: {e}[/red]")
67
+ raise typer.Exit(1)
68
+
69
+
70
+ @batch_app.command("create")
71
+ def create_batch(
72
+ input_file_id: str = typer.Argument(..., help="File ID of uploaded JSONL file"),
73
+ endpoint: Optional[str] = typer.Option(None, "--endpoint", "-e", help="API endpoint (optional, uses URLs from JSONL if not specified)"),
74
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use for all requests (overrides model in JSONL)"),
75
+ completion_window: str = typer.Option("24h", "--completion-window", "-w", help="Completion window (24h only)"),
76
+ ):
77
+ """Create a batch processing job"""
78
+ try:
79
+ console.print(f"[dim]🚀 Creating batch job for file {input_file_id}...[/dim]")
80
+
81
+ request_data = {
82
+ "input_file_id": input_file_id,
83
+ "completion_window": completion_window
84
+ }
85
+
86
+ # Only include endpoint if explicitly provided as override
87
+ if endpoint:
88
+ request_data["endpoint"] = endpoint
89
+ console.print(f"[dim]Using endpoint override: {endpoint}[/dim]")
90
+ # Note: endpoint is NOT required - the API should use URLs from JSONL file
91
+
92
+ # Include model override if specified
93
+ if model:
94
+ request_data["model"] = model
95
+ console.print(f"[dim]Using model override: {model}[/dim]")
96
+
97
+ response = httpx.post(
98
+ f"{config.base_url}/api/v2/external/batches",
99
+ headers={"Authorization": f"Bearer {config.api_key}"},
100
+ json=request_data,
101
+ timeout=30.0
102
+ )
103
+
104
+ if response.status_code != 200:
105
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
106
+ console.print(f"[red]{response.text}[/red]")
107
+ raise typer.Exit(1)
108
+
109
+ data = response.json()
110
+
111
+ console.print(f"[green]✅ Batch job created successfully![/green]")
112
+ console.print(f"[cyan]Batch ID: {data['id']}[/cyan]")
113
+ console.print(f"[yellow]Status: {data['status']}[/yellow]")
114
+ console.print(f"[dim]Endpoint: {data['endpoint']}[/dim]")
115
+ console.print(f"[dim]Input File ID: {data['input_file_id']}[/dim]")
116
+ console.print(f"[dim]Expires: {data['expires_at']}[/dim]")
117
+
118
+ except Exception as e:
119
+ console.print(f"[red]Error: {e}[/red]")
120
+ raise typer.Exit(1)
121
+
122
+
123
+ @batch_app.command("get")
124
+ def get_batch(
125
+ batch_id: str = typer.Argument(..., help="Batch ID to retrieve"),
126
+ ):
127
+ """Get batch job status and details"""
128
+ try:
129
+ console.print(f"[dim]🔍 Retrieving batch {batch_id}...[/dim]")
130
+
131
+ response = httpx.get(
132
+ f"{config.base_url}/api/v2/external/batches/{batch_id}",
133
+ headers={"Authorization": f"Bearer {config.api_key}"},
134
+ timeout=30.0
135
+ )
136
+
137
+ if response.status_code != 200:
138
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
139
+ console.print(f"[red]{response.text}[/red]")
140
+ raise typer.Exit(1)
141
+
142
+ data = response.json()
143
+
144
+ # Status color coding
145
+ status = data['status']
146
+ if status == "completed":
147
+ status_color = "green"
148
+ elif status in ["failed", "expired", "cancelled"]:
149
+ status_color = "red"
150
+ elif status in ["in_progress", "finalizing"]:
151
+ status_color = "yellow"
152
+ else:
153
+ status_color = "dim"
154
+
155
+ console.print(f"[cyan]Batch ID: {data['id']}[/cyan]")
156
+ console.print(f"[{status_color}]Status: {status}[/{status_color}]")
157
+ console.print(f"[dim]Endpoint: {data['endpoint']}[/dim]")
158
+ console.print(f"[dim]Input File: {data['input_file_id']}[/dim]")
159
+
160
+ if data.get('output_file_id'):
161
+ console.print(f"[green]Output File: {data['output_file_id']}[/green]")
162
+
163
+ if data.get('error_file_id'):
164
+ console.print(f"[red]Error File: {data['error_file_id']}[/red]")
165
+
166
+ # Request statistics
167
+ counts = data.get('request_counts', {})
168
+ console.print(f"[dim]Requests - Total: {counts.get('total', 0)}, Completed: {counts.get('completed', 0)}, Failed: {counts.get('failed', 0)}[/dim]")
169
+
170
+ # Timestamps
171
+ console.print(f"[dim]Created: {data.get('created_at', 'N/A')}[/dim]")
172
+ if data.get('completed_at'):
173
+ console.print(f"[dim]Completed: {data['completed_at']}[/dim]")
174
+ if data.get('expires_at'):
175
+ console.print(f"[dim]Expires: {data['expires_at']}[/dim]")
176
+
177
+ except Exception as e:
178
+ console.print(f"[red]Error: {e}[/red]")
179
+ raise typer.Exit(1)
180
+
181
+
182
+ @batch_app.command("list")
183
+ def list_batches(
184
+ after: Optional[str] = typer.Option(None, "--after", help="List batches after this batch ID"),
185
+ limit: int = typer.Option(20, "--limit", "-l", help="Number of batches to return"),
186
+ ):
187
+ """List batch jobs"""
188
+ try:
189
+ console.print("[dim]📋 Listing batch jobs...[/dim]")
190
+
191
+ params = {"limit": limit}
192
+ if after:
193
+ params["after"] = after
194
+
195
+ response = httpx.get(
196
+ f"{config.base_url}/api/v2/external/batches",
197
+ headers={"Authorization": f"Bearer {config.api_key}"},
198
+ params=params,
199
+ timeout=30.0
200
+ )
201
+
202
+ if response.status_code != 200:
203
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
204
+ console.print(f"[red]{response.text}[/red]")
205
+ raise typer.Exit(1)
206
+
207
+ data = response.json()
208
+ batches = data.get('data', [])
209
+
210
+ if not batches:
211
+ console.print("[dim]No batch jobs found[/dim]")
212
+ return
213
+
214
+ columns = [
215
+ {"header": "Batch ID", "style": "cyan", "no_wrap": True, "max_width": 16},
216
+ {"header": "Status", "style": "yellow"},
217
+ {"header": "Endpoint", "style": "green"},
218
+ {"header": "Requests", "style": "magenta", "justify": "center"},
219
+ {"header": "Created", "style": "dim"}
220
+ ]
221
+
222
+ table = create_table("Batch Jobs", columns)
223
+
224
+ for batch in batches:
225
+ batch_id = batch['id']
226
+ short_id = truncate_id(batch_id, 12)
227
+
228
+ counts = batch.get('request_counts', {})
229
+ request_stats = f"{counts.get('completed', 0)}/{counts.get('total', 0)}"
230
+
231
+ table.add_row(
232
+ short_id,
233
+ batch['status'],
234
+ batch['endpoint'],
235
+ request_stats,
236
+ format_timestamp(batch.get('created_at'))
237
+ )
238
+
239
+ console.print(table)
240
+ console.print(f"\n[dim]Found {len(batches)} batch jobs[/dim]")
241
+
242
+ except Exception as e:
243
+ console.print(f"[red]Error: {e}[/red]")
244
+ raise typer.Exit(1)
245
+
246
+
247
+ @batch_app.command("cancel")
248
+ def cancel_batch(
249
+ batch_id: str = typer.Argument(..., help="Batch ID to cancel"),
250
+ ):
251
+ """Cancel a batch job"""
252
+ try:
253
+ console.print(f"[dim]🛑 Cancelling batch {batch_id}...[/dim]")
254
+
255
+ response = httpx.post(
256
+ f"{config.base_url}/api/v2/external/batches/{batch_id}/cancel",
257
+ headers={"Authorization": f"Bearer {config.api_key}"},
258
+ timeout=30.0
259
+ )
260
+
261
+ if response.status_code != 200:
262
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
263
+ console.print(f"[red]{response.text}[/red]")
264
+ raise typer.Exit(1)
265
+
266
+ data = response.json()
267
+
268
+ console.print(f"[green]✅ Batch cancelled successfully![/green]")
269
+ console.print(f"[cyan]Batch ID: {data['id']}[/cyan]")
270
+ console.print(f"[yellow]Status: {data['status']}[/yellow]")
271
+
272
+ except Exception as e:
273
+ console.print(f"[red]Error: {e}[/red]")
274
+ raise typer.Exit(1)
275
+
276
+
277
+ @batch_app.command("download")
278
+ def download_file(
279
+ file_id: str = typer.Argument(..., help="File ID to download"),
280
+ output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path (prints to console if not specified)"),
281
+ ):
282
+ """Download batch file content (input, output, or error files)"""
283
+ try:
284
+ console.print(f"[dim]⬇️ Downloading file {file_id}...[/dim]")
285
+
286
+ response = httpx.get(
287
+ f"{config.base_url}/api/v2/external/files/{file_id}/content",
288
+ headers={"Authorization": f"Bearer {config.api_key}"},
289
+ timeout=60.0
290
+ )
291
+
292
+ if response.status_code != 200:
293
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
294
+ console.print(f"[red]{response.text}[/red]")
295
+ raise typer.Exit(1)
296
+
297
+ content = response.text
298
+
299
+ if output_file:
300
+ # Save to file
301
+ with open(output_file, 'w') as f:
302
+ f.write(content)
303
+ console.print(f"[green]✅ Content saved to {output_file}[/green]")
304
+ console.print(f"[dim]Size: {len(content)} characters[/dim]")
305
+ else:
306
+ # Print to console
307
+ console.print("[green]📄 File Content:[/green]")
308
+ console.print("-" * 50)
309
+ console.print(content)
310
+ console.print("-" * 50)
311
+ console.print(f"[dim]Size: {len(content)} characters[/dim]")
312
+
313
+ except Exception as e:
314
+ console.print(f"[red]Error: {e}[/red]")
315
+ raise typer.Exit(1)
@@ -0,0 +1,220 @@
1
+ """
2
+ Synchronous inference commands for chat/completion
3
+ """
4
+
5
+ from typing import Optional
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ import json
10
+
11
+ from ....shared.config import config
12
+
13
+ console = Console()
14
+
15
+ # Import generated client modules
16
+ # We'll use direct HTTP calls since the sync inference models aren't generated yet
17
+
18
+ chat_app = typer.Typer(name="chat", help="Synchronous inference commands")
19
+
20
+
21
+ @chat_app.command("send")
22
+ def send_message(
23
+ message: str = typer.Argument(..., help="Message to send to the model"),
24
+ model: str = typer.Option("gpt-4", "--model", "-m", help="Model to use for inference"),
25
+ max_tokens: int = typer.Option(1000, "--max-tokens", help="Maximum tokens in response"),
26
+ temperature: float = typer.Option(0.7, "--temperature", "-t", help="Temperature (0.0-2.0)"),
27
+ system_prompt: Optional[str] = typer.Option(None, "--system", "-s", help="System prompt"),
28
+ ):
29
+ """Send a message to an AI model and get a response"""
30
+ try:
31
+ client = config.get_client()
32
+
33
+ # Create the sync request payload directly
34
+ sync_request = {
35
+ "model_id": model,
36
+ "input": {
37
+ "text": message,
38
+ "parameters": {"system_prompt": system_prompt} if system_prompt else {}
39
+ },
40
+ "max_tokens": max_tokens,
41
+ "temperature": temperature,
42
+ "top_p": 1.0,
43
+ "stream": False
44
+ }
45
+
46
+ console.print(f"[dim]🤖 Sending message to {model}...[/dim]")
47
+
48
+ # Make the API call using httpx directly (since we don't have generated client for sync inference yet)
49
+ import httpx
50
+
51
+ url = f"{config.base_url}/api/v2/external/sync/"
52
+ headers = {"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}
53
+
54
+ with httpx.Client() as http_client:
55
+ response = http_client.post(
56
+ url,
57
+ json=sync_request,
58
+ headers=headers,
59
+ timeout=60.0
60
+ )
61
+
62
+ if response.status_code == 200:
63
+ result = response.json()
64
+
65
+ console.print(f"[green]✅ Response from {model}:[/green]")
66
+ console.print(f"[cyan]{result['output']}[/cyan]")
67
+
68
+ # Show usage stats
69
+ if 'usage' in result:
70
+ usage = result['usage']
71
+ console.print(f"[dim]📊 Tokens: {usage.get('total_tokens', 0)} | "
72
+ f"Latency: {result.get('latency_ms', 0)}ms | "
73
+ f"Cost: ${result.get('cost', 0):.4f}[/dim]")
74
+
75
+ elif response.status_code == 503:
76
+ console.print(f"[red]❌ Model {model} is not running. Please contact support to start the model.[/red]")
77
+ raise typer.Exit(1)
78
+ else:
79
+ console.print(f"[red]❌ Error: HTTP {response.status_code}[/red]")
80
+ console.print(f"[red]{response.text}[/red]")
81
+ raise typer.Exit(1)
82
+
83
+ except Exception as e:
84
+ console.print(f"[red]❌ Error: {e}[/red]")
85
+ raise typer.Exit(1)
86
+
87
+
88
+ @chat_app.command("models")
89
+ def list_models():
90
+ """List available models for inference"""
91
+ try:
92
+ client = config.get_client()
93
+
94
+ # Make API call to get available models
95
+ import httpx
96
+
97
+ url = f"{config.base_url}/api/v2/external/models/"
98
+ headers = {"Authorization": f"Bearer {config.api_key}"}
99
+
100
+ with httpx.Client() as http_client:
101
+ response = http_client.get(url, headers=headers, timeout=10.0)
102
+
103
+ if response.status_code == 200:
104
+ models = response.json()
105
+
106
+ if models:
107
+ # Create a table
108
+ table = Table(title="Available AI Models")
109
+ table.add_column("Model", style="cyan", no_wrap=True)
110
+ table.add_column("Type", style="magenta")
111
+ table.add_column("Status", justify="center")
112
+ table.add_column("Price/1K tokens", justify="right", style="green")
113
+
114
+ # Sort models: running first, then by type, then by name
115
+ sorted_models = sorted(models, key=lambda m: (
116
+ not m.get('available', False), # Running models first
117
+ m.get('type', 'text'), # Then by type
118
+ m.get('model_id', '') # Then by name
119
+ ))
120
+
121
+ for model in sorted_models:
122
+ # Status with emoji
123
+ status = "🟢 Running" if model.get('available') else "🔴 Stopped"
124
+
125
+ # Model type with emoji
126
+ model_type = model.get('type', 'text')
127
+ type_emoji = {
128
+ 'text': 'Text',
129
+ 'image': 'Image',
130
+ 'audio': 'Audio',
131
+ 'multimodal': 'Multi',
132
+ 'embedding': 'Embed'
133
+ }.get(model_type, f'❓ {model_type.title()}')
134
+
135
+ # Provider
136
+ provider = model.get('provider', 'unknown').title()
137
+
138
+ # Price
139
+ price = model.get('price_per_1k_tokens', 0)
140
+ price_str = f"${price:.4f}" if price > 0 else "Free"
141
+
142
+ table.add_row(
143
+ model.get('model_id', 'Unknown'),
144
+ type_emoji,
145
+ status,
146
+ price_str
147
+ )
148
+
149
+ console.print(table)
150
+
151
+ # Show summary
152
+ running_count = sum(1 for m in models if m.get('available'))
153
+ total_count = len(models)
154
+ console.print(f"\n[dim]📊 {running_count}/{total_count} models running[/dim]")
155
+
156
+ else:
157
+ console.print("[yellow]No models are currently available[/yellow]")
158
+ else:
159
+ console.print(f"[red]❌ Error: HTTP {response.status_code}[/red]")
160
+ console.print(f"[red]{response.text}[/red]")
161
+ raise typer.Exit(1)
162
+
163
+ except Exception as e:
164
+ console.print(f"[red]❌ Error: {e}[/red]")
165
+ raise typer.Exit(1)
166
+
167
+
168
+ @chat_app.command("image")
169
+ def analyze_image(
170
+ image_url: str = typer.Argument(..., help="URL of image to analyze"),
171
+ prompt: str = typer.Option("What do you see in this image?", "--prompt", "-p", help="Question about the image"),
172
+ model: str = typer.Option("gpt-4-vision", "--model", "-m", help="Vision model to use"),
173
+ ):
174
+ """Analyze an image with AI vision models"""
175
+ try:
176
+ client = config.get_client()
177
+
178
+ # Create request payload for image analysis
179
+ sync_request = {
180
+ "model_id": model,
181
+ "input": {
182
+ "text": prompt,
183
+ "image_url": image_url
184
+ },
185
+ "max_tokens": 1000,
186
+ "temperature": 0.7
187
+ }
188
+
189
+ console.print(f"[dim]👁️ Analyzing image with {model}...[/dim]")
190
+
191
+ import httpx
192
+
193
+ url = f"{config.base_url}/api/v2/external/sync/"
194
+ headers = {"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}
195
+
196
+ with httpx.Client() as http_client:
197
+ response = http_client.post(
198
+ url,
199
+ json=sync_request,
200
+ headers=headers,
201
+ timeout=60.0
202
+ )
203
+
204
+ if response.status_code == 200:
205
+ result = response.json()
206
+
207
+ console.print(f"[green]✅ Image Analysis:[/green]")
208
+ console.print(f"[cyan]{result['output']}[/cyan]")
209
+
210
+ elif response.status_code == 503:
211
+ console.print(f"[red]❌ Vision model {model} is not running.[/red]")
212
+ raise typer.Exit(1)
213
+ else:
214
+ console.print(f"[red]❌ Error: HTTP {response.status_code}[/red]")
215
+ console.print(f"[red]{response.text}[/red]")
216
+ raise typer.Exit(1)
217
+
218
+ except Exception as e:
219
+ console.print(f"[red]❌ Error: {e}[/red]")
220
+ raise typer.Exit(1)