commoncompute 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.
- commoncompute/__init__.py +86 -0
- commoncompute/_generated_workloads.py +60 -0
- commoncompute/_transport.py +359 -0
- commoncompute/aws_batch.py +589 -0
- commoncompute/cli.py +474 -0
- commoncompute/client.py +591 -0
- commoncompute/compat/__init__.py +12 -0
- commoncompute/compat/openai.py +54 -0
- commoncompute/connect.py +140 -0
- commoncompute/errors.py +131 -0
- commoncompute/models.py +152 -0
- commoncompute/tasks.py +365 -0
- commoncompute-0.1.0.dist-info/METADATA +210 -0
- commoncompute-0.1.0.dist-info/RECORD +17 -0
- commoncompute-0.1.0.dist-info/WHEEL +4 -0
- commoncompute-0.1.0.dist-info/entry_points.txt +3 -0
- commoncompute-0.1.0.dist-info/licenses/LICENSE +21 -0
commoncompute/cli.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""`cc` command-line interface.
|
|
2
|
+
|
|
3
|
+
Installed as ``cc`` and ``commoncompute`` via the pyproject entry point.
|
|
4
|
+
Uses Typer for argparsing + Rich for pretty output. Reads credentials
|
|
5
|
+
from (in order): ``--api-key`` flag, ``CC_API_KEY`` env var, or the
|
|
6
|
+
config file at ``~/.config/commoncompute/credentials`` (a single line).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import webbrowser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
except ImportError: # CLI deps are the [cli] extra, not core deps
|
|
24
|
+
print(
|
|
25
|
+
"The `cc` CLI needs the cli extra: pip install \"commoncompute[cli]\"",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
raise SystemExit(2)
|
|
29
|
+
|
|
30
|
+
from .client import Client
|
|
31
|
+
from .errors import CommonComputeError
|
|
32
|
+
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="cc",
|
|
35
|
+
add_completion=False,
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
help="Common Compute CLI — submit jobs, check spend, manage keys.",
|
|
38
|
+
rich_markup_mode="rich",
|
|
39
|
+
)
|
|
40
|
+
jobs_app = typer.Typer(no_args_is_help=True, help="Inspect and submit jobs.")
|
|
41
|
+
keys_app = typer.Typer(no_args_is_help=True, help="Manage API keys.")
|
|
42
|
+
receipts_app = typer.Typer(no_args_is_help=True, help="List, verify, and export job receipts.")
|
|
43
|
+
app.add_typer(jobs_app, name="jobs")
|
|
44
|
+
app.add_typer(keys_app, name="keys")
|
|
45
|
+
app.add_typer(receipts_app, name="receipts")
|
|
46
|
+
|
|
47
|
+
console = Console()
|
|
48
|
+
err_console = Console(stderr=True, style="red")
|
|
49
|
+
|
|
50
|
+
CONFIG_DIR = Path(os.environ.get("CC_CONFIG_DIR") or (Path.home() / ".config" / "commoncompute"))
|
|
51
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_api_key(override: Optional[str] = None) -> Optional[str]:
|
|
55
|
+
if override:
|
|
56
|
+
return override
|
|
57
|
+
env = os.environ.get("CC_API_KEY")
|
|
58
|
+
if env:
|
|
59
|
+
return env
|
|
60
|
+
if CREDENTIALS_FILE.exists():
|
|
61
|
+
return CREDENTIALS_FILE.read_text().strip() or None
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _client(api_key: Optional[str] = None) -> Client:
|
|
66
|
+
key = _load_api_key(api_key)
|
|
67
|
+
if not key:
|
|
68
|
+
err_console.print("No API key found. Run [bold]cc login[/bold] or set [bold]CC_API_KEY[/bold].")
|
|
69
|
+
raise typer.Exit(2)
|
|
70
|
+
try:
|
|
71
|
+
return Client(api_key=key)
|
|
72
|
+
except CommonComputeError as e:
|
|
73
|
+
err_console.print(str(e))
|
|
74
|
+
raise typer.Exit(2)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _fmt_usd(cents: int) -> str:
|
|
78
|
+
return f"${cents / 100:,.2f}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _print_err(e: Exception) -> None:
|
|
82
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ─── login / config ─────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def login(
|
|
89
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="Paste a cc_live_… or cc_test_… key instead of using the browser."),
|
|
90
|
+
no_browser: bool = typer.Option(False, "--no-browser", help="Print the approval URL instead of opening the browser."),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Connect this machine to your account (opens the browser to approve).
|
|
93
|
+
|
|
94
|
+
Pass --api-key to paste an existing key instead.
|
|
95
|
+
"""
|
|
96
|
+
if api_key:
|
|
97
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
CREDENTIALS_FILE.write_text(api_key + "\n")
|
|
99
|
+
CREDENTIALS_FILE.chmod(0o600)
|
|
100
|
+
console.print(f"[green]✓[/green] Saved credentials to {CREDENTIALS_FILE}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
from .connect import connect as _connect
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
_connect(open_browser=not no_browser)
|
|
107
|
+
except KeyboardInterrupt:
|
|
108
|
+
err_console.print("Login aborted.")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
except CommonComputeError as e:
|
|
111
|
+
_print_err(e)
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def logout() -> None:
|
|
117
|
+
"""Remove the stored API key."""
|
|
118
|
+
if CREDENTIALS_FILE.exists():
|
|
119
|
+
CREDENTIALS_FILE.unlink()
|
|
120
|
+
console.print(f"[green]✓[/green] Removed {CREDENTIALS_FILE}")
|
|
121
|
+
else:
|
|
122
|
+
console.print("Nothing to remove.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command()
|
|
126
|
+
def whoami() -> None:
|
|
127
|
+
"""Show the account behind the current API key."""
|
|
128
|
+
c = _client()
|
|
129
|
+
try:
|
|
130
|
+
me = c.me()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
_print_err(e); raise typer.Exit(1)
|
|
133
|
+
console.print(Panel.fit(
|
|
134
|
+
f"[bold]{me.get('full_name') or me.get('email')}[/bold]\n"
|
|
135
|
+
f"[dim]{me.get('email')}[/dim]\n"
|
|
136
|
+
f"role={me.get('role', '?')} verified={bool(me.get('email_verified_at'))}",
|
|
137
|
+
title="whoami",
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ─── balance / usage ────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def balance() -> None:
|
|
145
|
+
"""Show the current workspace balance."""
|
|
146
|
+
c = _client()
|
|
147
|
+
try:
|
|
148
|
+
bal = c.balance()
|
|
149
|
+
except Exception as e:
|
|
150
|
+
_print_err(e); raise typer.Exit(1)
|
|
151
|
+
table = Table(show_header=False, box=None)
|
|
152
|
+
if "has_card" in bal:
|
|
153
|
+
# Cash-only billing: the endpoint reports card-on-file state.
|
|
154
|
+
card = f"{bal.get('brand') or 'card'} ••••{bal.get('last4')}" if bal.get("last4") else ("on file" if bal.get("has_card") else "none")
|
|
155
|
+
table.add_row("Payment method", card)
|
|
156
|
+
table.add_row("Can submit jobs", "yes" if bal.get("can_submit_jobs") else "no — add a card")
|
|
157
|
+
else: # legacy prepaid shape
|
|
158
|
+
table.add_row("Balance", _fmt_usd(bal.get("balance_cents", 0)))
|
|
159
|
+
table.add_row("Total topped up", _fmt_usd(bal.get("lifetime_topup_cents", 0)))
|
|
160
|
+
table.add_row("Total spent", _fmt_usd(bal.get("lifetime_spend_cents", 0)))
|
|
161
|
+
console.print(table)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command()
|
|
165
|
+
def workloads() -> None:
|
|
166
|
+
"""List the available workloads."""
|
|
167
|
+
c = _client()
|
|
168
|
+
try:
|
|
169
|
+
items = c.workloads.list()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
_print_err(e); raise typer.Exit(1)
|
|
172
|
+
t = Table("ID", "Label", "Family", "Status", title="Workloads")
|
|
173
|
+
for w in items:
|
|
174
|
+
t.add_row(w.get("id", "?"), w.get("label", ""), w.get("family", ""), w.get("status", ""))
|
|
175
|
+
console.print(t)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def models(workload: Optional[str] = typer.Option(None, "--workload", "-w", help="Filter by workload id.")) -> None:
|
|
180
|
+
"""List models, optionally filtered by workload."""
|
|
181
|
+
c = _client()
|
|
182
|
+
try:
|
|
183
|
+
items = c.models.list(workload_id=workload)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
_print_err(e); raise typer.Exit(1)
|
|
186
|
+
t = Table("ID", "Label", "Workload", "Min memory", title="Models")
|
|
187
|
+
for m in items:
|
|
188
|
+
mem = m.get("minMemoryGb") or m.get("min_memory_gb")
|
|
189
|
+
t.add_row(m.get("id", "?"), m.get("label", ""), m.get("workloadId", m.get("workload_id", "")), f"{mem} GB" if mem else "—")
|
|
190
|
+
console.print(t)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ─── quote / submit ─────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def quote(
|
|
197
|
+
workload: str = typer.Argument(..., help="Workload id, e.g. coreml_embed."),
|
|
198
|
+
units: float = typer.Option(1, "--units", "-u"),
|
|
199
|
+
priority: str = typer.Option("standard", "--priority", "-p"),
|
|
200
|
+
model: Optional[str] = typer.Option(None, "--model", "-m"),
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Get a price estimate without submitting."""
|
|
203
|
+
c = _client()
|
|
204
|
+
try:
|
|
205
|
+
q = c.jobs.quote(workload, units=units, priority=priority, model_id=model)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
_print_err(e); raise typer.Exit(1)
|
|
208
|
+
console.print(Panel.fit(
|
|
209
|
+
f"[bold]${q.get('estimated_usd', 0):.4f}[/bold]\n"
|
|
210
|
+
f"[dim]{units} units · {priority} · ~{q.get('estimated_latency_s', 0):.1f}s[/dim]",
|
|
211
|
+
title=f"{workload} quote",
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command()
|
|
216
|
+
def submit(
|
|
217
|
+
workload: str = typer.Argument(..., help="Workload id."),
|
|
218
|
+
payload: Optional[str] = typer.Option(None, "--payload", "-f", help="Path to JSON payload file, or @- for stdin."),
|
|
219
|
+
model: Optional[str] = typer.Option(None, "--model", "-m"),
|
|
220
|
+
priority: str = typer.Option("standard", "--priority", "-p"),
|
|
221
|
+
wait: bool = typer.Option(False, "--wait", help="Block until the job finishes."),
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Submit a native Common Compute job."""
|
|
224
|
+
body: object = None
|
|
225
|
+
if payload:
|
|
226
|
+
if payload == "@-":
|
|
227
|
+
body = json.load(sys.stdin)
|
|
228
|
+
elif payload.startswith("@"):
|
|
229
|
+
body = json.loads(Path(payload[1:]).read_text())
|
|
230
|
+
else:
|
|
231
|
+
body = json.loads(payload)
|
|
232
|
+
c = _client()
|
|
233
|
+
try:
|
|
234
|
+
job = c.jobs.submit(workload, payload=body, model_id=model, priority=priority)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
_print_err(e); raise typer.Exit(1)
|
|
237
|
+
console.print(f"[green]✓[/green] queued id=[bold]{job['id']}[/bold] state={job.get('state')}")
|
|
238
|
+
if wait:
|
|
239
|
+
try:
|
|
240
|
+
final = c.jobs.wait(job["id"])
|
|
241
|
+
except Exception as e:
|
|
242
|
+
_print_err(e); raise typer.Exit(1)
|
|
243
|
+
console.print(f"[green]✓[/green] {final.get('state')} in ~{final.get('duration_seconds', 0):.1f}s")
|
|
244
|
+
console.print_json(data=final)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ─── jobs ───────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
@jobs_app.command("list")
|
|
250
|
+
def jobs_list(
|
|
251
|
+
limit: int = typer.Option(25, "--limit", "-n", help="How many recent jobs to show."),
|
|
252
|
+
as_json: bool = typer.Option(False, "--json", help="Raw JSON output."),
|
|
253
|
+
) -> None:
|
|
254
|
+
"""List your recent submissions (newest first)."""
|
|
255
|
+
c = _client()
|
|
256
|
+
try:
|
|
257
|
+
raw = c._transport.request("GET", "/v1/me/submissions", params={"limit": limit})
|
|
258
|
+
except Exception as e:
|
|
259
|
+
_print_err(e); raise typer.Exit(1)
|
|
260
|
+
rows = raw.get("submissions") or raw.get("jobs") or raw.get("tasks") or []
|
|
261
|
+
if as_json:
|
|
262
|
+
console.print_json(data=rows)
|
|
263
|
+
return
|
|
264
|
+
table = Table(box=None, header_style="bold")
|
|
265
|
+
for col in ("id", "workload", "state", "created"):
|
|
266
|
+
table.add_column(col)
|
|
267
|
+
for r in rows:
|
|
268
|
+
table.add_row(
|
|
269
|
+
str(r.get("id", ""))[:12],
|
|
270
|
+
str(r.get("type") or r.get("workload_id") or ""),
|
|
271
|
+
str(r.get("state", "")),
|
|
272
|
+
str(r.get("created_at", "")),
|
|
273
|
+
)
|
|
274
|
+
console.print(table)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@jobs_app.command("get")
|
|
278
|
+
def jobs_get(task_id: str) -> None:
|
|
279
|
+
"""Fetch a job by id."""
|
|
280
|
+
c = _client()
|
|
281
|
+
try:
|
|
282
|
+
job = c.jobs.get(task_id)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
_print_err(e); raise typer.Exit(1)
|
|
285
|
+
console.print_json(data=job)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@jobs_app.command("wait")
|
|
289
|
+
def jobs_wait(task_id: str, timeout: float = typer.Option(300, "--timeout", "-t")) -> None:
|
|
290
|
+
"""Poll a job until it reaches a terminal state."""
|
|
291
|
+
c = _client()
|
|
292
|
+
try:
|
|
293
|
+
final = c.jobs.wait(task_id, timeout=timeout)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
_print_err(e); raise typer.Exit(1)
|
|
296
|
+
console.print_json(data=final)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@jobs_app.command("download")
|
|
300
|
+
def jobs_download(task_id: str, out: Optional[str] = typer.Option(None, "--out", "-o")) -> None:
|
|
301
|
+
"""Download a job's result. With --out, writes to disk; otherwise stdout."""
|
|
302
|
+
c = _client()
|
|
303
|
+
try:
|
|
304
|
+
result = c.jobs.download(task_id, dest=out)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
_print_err(e); raise typer.Exit(1)
|
|
307
|
+
if isinstance(result, Path):
|
|
308
|
+
console.print(f"[green]✓[/green] wrote {result}")
|
|
309
|
+
else:
|
|
310
|
+
sys.stdout.buffer.write(result)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ─── receipts ───────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
@receipts_app.command("list")
|
|
316
|
+
def receipts_list(limit: int = typer.Option(50, "--limit", "-n")) -> None:
|
|
317
|
+
"""List receipts for completed jobs."""
|
|
318
|
+
c = _client()
|
|
319
|
+
try:
|
|
320
|
+
rows = c.receipts.list(limit=limit)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
_print_err(e); raise typer.Exit(1)
|
|
323
|
+
table = Table(box=None, header_style="bold")
|
|
324
|
+
for col in ("job", "cost", "provider", "timestamp"):
|
|
325
|
+
table.add_column(col)
|
|
326
|
+
for r in rows:
|
|
327
|
+
table.add_row(
|
|
328
|
+
r.job_id[:12],
|
|
329
|
+
f"${r.cost_usd:.4f}" if r.cost_usd is not None else "—",
|
|
330
|
+
(r.provider_id or "—")[:12],
|
|
331
|
+
r.timestamp or "—",
|
|
332
|
+
)
|
|
333
|
+
console.print(table)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@receipts_app.command("export")
|
|
337
|
+
def receipts_export(
|
|
338
|
+
fmt: str = typer.Option("csv", "--format", "-f", help="csv or json"),
|
|
339
|
+
out: Optional[str] = typer.Option(None, "--out", "-o", help="Write to file instead of stdout."),
|
|
340
|
+
limit: int = typer.Option(1000, "--limit", "-n"),
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Export receipts as CSV or JSON."""
|
|
343
|
+
c = _client()
|
|
344
|
+
try:
|
|
345
|
+
blob = c.receipts.export(format=fmt, limit=limit)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
_print_err(e); raise typer.Exit(1)
|
|
348
|
+
if out:
|
|
349
|
+
Path(out).write_text(blob)
|
|
350
|
+
console.print(f"[green]✓[/green] wrote {out}")
|
|
351
|
+
else:
|
|
352
|
+
sys.stdout.write(blob)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@app.command()
|
|
356
|
+
def estimate(
|
|
357
|
+
workload: str = typer.Argument(..., help="Workload id, e.g. vision_ocr"),
|
|
358
|
+
units: float = typer.Option(1, "--units", "-u", help="Billing units (see /v1/workloads for the unit)."),
|
|
359
|
+
priority: str = typer.Option("batch", "--priority", "-p"),
|
|
360
|
+
model: Optional[str] = typer.Option(None, "--model", "-m"),
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Price + ETA for a job without executing it (dry run)."""
|
|
363
|
+
c = _client()
|
|
364
|
+
try:
|
|
365
|
+
q = c.jobs.quote(workload, units=units, priority=priority, model_id=model)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
_print_err(e); raise typer.Exit(1)
|
|
368
|
+
console.print(
|
|
369
|
+
Panel.fit(
|
|
370
|
+
f"[bold]${q.get('estimated_usd', 0):.4f}[/bold] "
|
|
371
|
+
f"({q.get('unit_price_usd', 0)} per unit × {units})\n"
|
|
372
|
+
f"ETA ~{q.get('estimated_latency_s', 0):.1f}s · priority {q.get('priority', priority)}",
|
|
373
|
+
title=workload,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ─── keys ───────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
@keys_app.command("list")
|
|
381
|
+
def keys_list() -> None:
|
|
382
|
+
c = _client()
|
|
383
|
+
try:
|
|
384
|
+
keys = c.keys.list()
|
|
385
|
+
except Exception as e:
|
|
386
|
+
_print_err(e); raise typer.Exit(1)
|
|
387
|
+
t = Table("Prefix", "Name", "Mode", "Created", "Last used", title="API keys")
|
|
388
|
+
for k in keys:
|
|
389
|
+
t.add_row(
|
|
390
|
+
k.get("prefix", "?"),
|
|
391
|
+
k.get("name") or "—",
|
|
392
|
+
k.get("mode", "live"),
|
|
393
|
+
str(k.get("created_at", "")),
|
|
394
|
+
str(k.get("last_used_at", "—") or "—"),
|
|
395
|
+
)
|
|
396
|
+
console.print(t)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@keys_app.command("create")
|
|
400
|
+
def keys_create(
|
|
401
|
+
name: str = typer.Option(..., "--name", "-n"),
|
|
402
|
+
mode: str = typer.Option("live", "--mode", help="live or test"),
|
|
403
|
+
) -> None:
|
|
404
|
+
c = _client()
|
|
405
|
+
try:
|
|
406
|
+
created = c.keys.create(name=name, mode=mode)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
_print_err(e); raise typer.Exit(1)
|
|
409
|
+
full = created.get("key") or created.get("full_key")
|
|
410
|
+
if full:
|
|
411
|
+
console.print(Panel(
|
|
412
|
+
f"[bold yellow]Copy this key now — it will not be shown again.[/bold yellow]\n\n[bold]{full}[/bold]",
|
|
413
|
+
title=f"new key · {name}",
|
|
414
|
+
))
|
|
415
|
+
else:
|
|
416
|
+
console.print_json(data=created)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@keys_app.command("revoke")
|
|
420
|
+
def keys_revoke(key_id: str) -> None:
|
|
421
|
+
c = _client()
|
|
422
|
+
try:
|
|
423
|
+
c.keys.revoke(key_id)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
_print_err(e); raise typer.Exit(1)
|
|
426
|
+
console.print(f"[green]✓[/green] revoked {key_id}")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ─── chat / embed ───────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
@app.command()
|
|
432
|
+
def chat(
|
|
433
|
+
prompt: str = typer.Argument(..., help="Your message."),
|
|
434
|
+
model: str = typer.Option("qwen-2.5-7b-instruct", "--model", "-m"),
|
|
435
|
+
stream: bool = typer.Option(True, "--stream/--no-stream"),
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Send a one-shot chat message to a model."""
|
|
438
|
+
c = _client()
|
|
439
|
+
try:
|
|
440
|
+
if stream:
|
|
441
|
+
for chunk in c.chat.stream(model=model, messages=[{"role": "user", "content": prompt}]):
|
|
442
|
+
delta = chunk.get("choices", [{}])[0].get("delta", {}).get("content", "") or ""
|
|
443
|
+
sys.stdout.write(delta)
|
|
444
|
+
sys.stdout.flush()
|
|
445
|
+
sys.stdout.write("\n")
|
|
446
|
+
else:
|
|
447
|
+
reply = c.chat.create(model=model, messages=[{"role": "user", "content": prompt}])
|
|
448
|
+
print(reply["choices"][0]["message"]["content"])
|
|
449
|
+
except Exception as e:
|
|
450
|
+
_print_err(e); raise typer.Exit(1)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@app.command()
|
|
454
|
+
def embed(text: str = typer.Argument(...), model: Optional[str] = typer.Option(None, "--model", "-m")) -> None:
|
|
455
|
+
"""Generate an embedding for a single string (prints the vector length)."""
|
|
456
|
+
c = _client()
|
|
457
|
+
try:
|
|
458
|
+
res = c.embeddings.create(input=text, model=model)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
_print_err(e); raise typer.Exit(1)
|
|
461
|
+
vec = res.get("data", [{}])[0].get("embedding", [])
|
|
462
|
+
console.print(f"[green]✓[/green] dims={len(vec)} model={res.get('model')} tokens={res.get('usage', {}).get('total_tokens', '?')}")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@app.command()
|
|
466
|
+
def playground() -> None:
|
|
467
|
+
"""Open the web playground."""
|
|
468
|
+
url = os.environ.get("CC_WEB_BASE", "https://commoncompute.ai") + "/app/playground"
|
|
469
|
+
webbrowser.open(url)
|
|
470
|
+
console.print(f"Opened {url}")
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
if __name__ == "__main__":
|
|
474
|
+
app()
|