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.
- lyceum/__init__.py +0 -0
- lyceum/external/__init__.py +0 -0
- lyceum/external/auth/__init__.py +0 -0
- lyceum/external/auth/login.py +462 -0
- lyceum/external/compute/__init__.py +0 -0
- lyceum/external/compute/execution/__init__.py +0 -0
- lyceum/external/compute/execution/python.py +99 -0
- lyceum/external/compute/inference/__init__.py +2 -0
- lyceum/external/compute/inference/batch.py +315 -0
- lyceum/external/compute/inference/chat.py +220 -0
- lyceum/external/compute/inference/models.py +242 -0
- lyceum/external/general/__init__.py +0 -0
- lyceum/main.py +52 -0
- lyceum/shared/__init__.py +0 -0
- lyceum/shared/config.py +144 -0
- lyceum/shared/display.py +195 -0
- lyceum/shared/streaming.py +134 -0
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.18.dist-info/RECORD +25 -0
- lyceum_cli-1.0.18.dist-info/top_level.txt +2 -0
- lyceum_cloud_execution_api_client/__init__.py +0 -0
- lyceum_cloud_execution_api_client/api/__init__.py +0 -0
- lyceum_cloud_execution_api_client/models/__init__.py +105 -0
- lyceum_cli-1.0.14.dist-info/RECORD +0 -5
- lyceum_cli-1.0.14.dist-info/top_level.txt +0 -1
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/WHEEL +0 -0
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|