cometapi-cli 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.
- cometapi_cli/__init__.py +3 -0
- cometapi_cli/app.py +85 -0
- cometapi_cli/client.py +270 -0
- cometapi_cli/commands/__init__.py +1 -0
- cometapi_cli/commands/account.py +39 -0
- cometapi_cli/commands/balance.py +56 -0
- cometapi_cli/commands/chat.py +104 -0
- cometapi_cli/commands/chat_repl.py +229 -0
- cometapi_cli/commands/config_cmd.py +174 -0
- cometapi_cli/commands/doctor.py +144 -0
- cometapi_cli/commands/logs.py +326 -0
- cometapi_cli/commands/models.py +44 -0
- cometapi_cli/commands/repl.py +134 -0
- cometapi_cli/commands/stats.py +39 -0
- cometapi_cli/commands/tasks.py +130 -0
- cometapi_cli/commands/tokens.py +87 -0
- cometapi_cli/config.py +102 -0
- cometapi_cli/console.py +8 -0
- cometapi_cli/constants.py +55 -0
- cometapi_cli/errors.py +113 -0
- cometapi_cli/formatters.py +156 -0
- cometapi_cli/main.py +8 -0
- cometapi_cli-0.1.0.dist-info/METADATA +228 -0
- cometapi_cli-0.1.0.dist-info/RECORD +27 -0
- cometapi_cli-0.1.0.dist-info/WHEEL +4 -0
- cometapi_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cometapi_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Usage logs command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ..config import get_client
|
|
12
|
+
from ..constants import extract_items, format_ts, parse_date, quota_to_usd
|
|
13
|
+
from ..errors import handle_errors
|
|
14
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
15
|
+
|
|
16
|
+
LOG_TYPE_MAP = {
|
|
17
|
+
"unknown": 0,
|
|
18
|
+
"topup": 1,
|
|
19
|
+
"consume": 2,
|
|
20
|
+
"manage": 3,
|
|
21
|
+
"system": 4,
|
|
22
|
+
"error": 5,
|
|
23
|
+
"refund": 6,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_LOG_TYPE_NAMES = {v: k for k, v in LOG_TYPE_MAP.items()}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_other(other_raw: str | None) -> dict:
|
|
30
|
+
"""Parse the ``other`` JSON string from a log entry, returning {} on failure."""
|
|
31
|
+
if not other_raw:
|
|
32
|
+
return {}
|
|
33
|
+
try:
|
|
34
|
+
parsed = _json.loads(other_raw)
|
|
35
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
36
|
+
except (ValueError, TypeError):
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _find_log_by_id(
|
|
41
|
+
client: object,
|
|
42
|
+
*,
|
|
43
|
+
request_id: str,
|
|
44
|
+
log_type: int | None = None,
|
|
45
|
+
model_name: str | None = None,
|
|
46
|
+
token_name: str | None = None,
|
|
47
|
+
start_timestamp: int | None = None,
|
|
48
|
+
end_timestamp: int | None = None,
|
|
49
|
+
group: str | None = None,
|
|
50
|
+
max_pages: int = 10,
|
|
51
|
+
) -> dict | None:
|
|
52
|
+
"""Search through paginated logs to find the entry matching a request ID."""
|
|
53
|
+
for pg in range(1, max_pages + 1):
|
|
54
|
+
resp = client.list_logs( # type: ignore[union-attr]
|
|
55
|
+
page=pg,
|
|
56
|
+
page_size=100,
|
|
57
|
+
log_type=log_type,
|
|
58
|
+
model_name=model_name,
|
|
59
|
+
token_name=token_name,
|
|
60
|
+
start_timestamp=start_timestamp,
|
|
61
|
+
end_timestamp=end_timestamp,
|
|
62
|
+
group=group,
|
|
63
|
+
)
|
|
64
|
+
items = extract_items(resp)
|
|
65
|
+
if not items:
|
|
66
|
+
break
|
|
67
|
+
for item in items:
|
|
68
|
+
if item.get("request_id") == request_id:
|
|
69
|
+
return item
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _format_log_detail(log: dict) -> dict:
|
|
74
|
+
"""Build a rich key-value detail card from a single log entry."""
|
|
75
|
+
other = _parse_other(log.get("other"))
|
|
76
|
+
prompt_tokens = log.get("prompt_tokens", 0)
|
|
77
|
+
completion_tokens = log.get("completion_tokens", 0)
|
|
78
|
+
quota = log.get("quota", 0)
|
|
79
|
+
|
|
80
|
+
info: dict[str, str | int | float] = {}
|
|
81
|
+
info["Request ID"] = log.get("request_id", "") or "—"
|
|
82
|
+
info["Response ID"] = log.get("response_id", "") or "—"
|
|
83
|
+
info["Time"] = format_ts(log.get("created_at", 0))
|
|
84
|
+
info["Model"] = log.get("model_name", "") or "—"
|
|
85
|
+
info["Token Name"] = log.get("token_name", "") or "—"
|
|
86
|
+
info["Group"] = log.get("group", "") or "—"
|
|
87
|
+
info["Stream"] = "Yes" if log.get("is_stream") else "No"
|
|
88
|
+
|
|
89
|
+
# Tokens
|
|
90
|
+
info["Prompt Tokens"] = f"{prompt_tokens:,}"
|
|
91
|
+
info["Completion Tokens"] = f"{completion_tokens:,}"
|
|
92
|
+
cache_tokens = other.get("cache_tokens", 0)
|
|
93
|
+
if cache_tokens:
|
|
94
|
+
info["Cache Tokens"] = f"{cache_tokens:,}"
|
|
95
|
+
|
|
96
|
+
# Cost
|
|
97
|
+
info["Cost (USD)"] = quota_to_usd(quota)
|
|
98
|
+
info["Quota (raw)"] = f"{quota:,}"
|
|
99
|
+
|
|
100
|
+
# Pricing breakdown from 'other'
|
|
101
|
+
model_ratio = other.get("model_ratio")
|
|
102
|
+
if model_ratio is not None:
|
|
103
|
+
info["Model Ratio"] = model_ratio
|
|
104
|
+
completion_ratio = other.get("completion_ratio")
|
|
105
|
+
if completion_ratio is not None:
|
|
106
|
+
info["Completion Ratio"] = f"{completion_ratio}x"
|
|
107
|
+
group_ratio = other.get("group_ratio")
|
|
108
|
+
if group_ratio is not None:
|
|
109
|
+
info["Group Ratio"] = group_ratio
|
|
110
|
+
model_price = other.get("model_price")
|
|
111
|
+
if model_price is not None:
|
|
112
|
+
info["Model Price"] = "default" if model_price == -1 else model_price
|
|
113
|
+
cache_ratio = other.get("cache_ratio")
|
|
114
|
+
if cache_ratio is not None:
|
|
115
|
+
info["Cache Ratio"] = cache_ratio
|
|
116
|
+
|
|
117
|
+
# Timing
|
|
118
|
+
use_time = log.get("use_time", 0)
|
|
119
|
+
info["Duration"] = f"{use_time:,} ms" if use_time else "—"
|
|
120
|
+
frt = other.get("frt")
|
|
121
|
+
if frt and frt > 0:
|
|
122
|
+
info["First Token"] = f"{frt:,} ms"
|
|
123
|
+
|
|
124
|
+
# Path
|
|
125
|
+
request_path = other.get("request_path")
|
|
126
|
+
if request_path:
|
|
127
|
+
info["Endpoint"] = request_path
|
|
128
|
+
|
|
129
|
+
return info
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _format_log_row(log: dict, *, detail: bool = False) -> dict:
|
|
133
|
+
row: dict = {
|
|
134
|
+
"time": format_ts(log.get("created_at", 0)),
|
|
135
|
+
"type": _LOG_TYPE_NAMES.get(log.get("type", 0), "unknown"),
|
|
136
|
+
"model": log.get("model_name", "") or "—",
|
|
137
|
+
"token": log.get("token_name", "") or "—",
|
|
138
|
+
"prompt": log.get("prompt_tokens", 0),
|
|
139
|
+
"completion": log.get("completion_tokens", 0),
|
|
140
|
+
"cost": quota_to_usd(log.get("quota", 0)),
|
|
141
|
+
"duration_ms": log.get("use_time", 0),
|
|
142
|
+
"stream": "Yes" if log.get("is_stream") else "No",
|
|
143
|
+
}
|
|
144
|
+
if detail:
|
|
145
|
+
row["request_id"] = log.get("request_id", "") or "—"
|
|
146
|
+
row["response_id"] = log.get("response_id", "") or "—"
|
|
147
|
+
other = _parse_other(log.get("other"))
|
|
148
|
+
row["model_ratio"] = other.get("model_ratio", "—")
|
|
149
|
+
row["group_ratio"] = other.get("group_ratio", "—")
|
|
150
|
+
row["completion_ratio"] = other.get("completion_ratio", "—")
|
|
151
|
+
model_price = other.get("model_price")
|
|
152
|
+
row["model_price"] = model_price if model_price is not None else "—"
|
|
153
|
+
return row
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@handle_errors
|
|
157
|
+
def logs(
|
|
158
|
+
ctx: typer.Context,
|
|
159
|
+
model: Annotated[str | None, typer.Option("--model", "-m", help="Filter by model name.")] = None,
|
|
160
|
+
token_name: Annotated[str | None, typer.Option("--token-name", "-t", help="Filter by token name.")] = None,
|
|
161
|
+
log_type: Annotated[
|
|
162
|
+
str | None,
|
|
163
|
+
typer.Option("--type", help="Filter by type: consume, topup, error, refund, manage, system."),
|
|
164
|
+
] = None,
|
|
165
|
+
search: Annotated[str | None, typer.Option("--search", "-s", help="[deprecated] Search logs by keyword.")] = None,
|
|
166
|
+
start: Annotated[str | None, typer.Option("--start", help="Start date (YYYY-MM-DD, ISO 8601, or Unix ts).")] = None,
|
|
167
|
+
end: Annotated[str | None, typer.Option("--end", help="End date (YYYY-MM-DD, ISO 8601, or Unix ts).")] = None,
|
|
168
|
+
group: Annotated[str | None, typer.Option("--group", "-g", help="Filter by group.")] = None,
|
|
169
|
+
request_id: Annotated[
|
|
170
|
+
str | None,
|
|
171
|
+
typer.Option("--request-id", help="Look up cost by request ID (X-Cometapi-Request-Id header)."),
|
|
172
|
+
] = None,
|
|
173
|
+
detail: Annotated[
|
|
174
|
+
bool,
|
|
175
|
+
typer.Option("--detail", help="Show extended columns (request ID, pricing ratios)."),
|
|
176
|
+
] = False,
|
|
177
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number.")] = 1,
|
|
178
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Results per page.")] = 20,
|
|
179
|
+
export: Annotated[bool, typer.Option("--export", help="Export logs as CSV to stdout.")] = False,
|
|
180
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
181
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Show your usage logs (requires access token)."""
|
|
184
|
+
from ..console import console, err_console
|
|
185
|
+
|
|
186
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
187
|
+
client = get_client(require_access_token=True)
|
|
188
|
+
|
|
189
|
+
# Parse date options
|
|
190
|
+
start_ts = parse_date(start, "start") if start else None
|
|
191
|
+
end_ts = parse_date(end, "end") if end else None
|
|
192
|
+
|
|
193
|
+
# --request-id: find the full log entry and display detail card
|
|
194
|
+
if request_id:
|
|
195
|
+
if export:
|
|
196
|
+
err_console.print("[red]--request-id cannot be used with --export.[/]")
|
|
197
|
+
raise typer.Exit(code=2)
|
|
198
|
+
|
|
199
|
+
type_int = None
|
|
200
|
+
if log_type:
|
|
201
|
+
type_int = LOG_TYPE_MAP.get(log_type.lower())
|
|
202
|
+
|
|
203
|
+
log_entry = _find_log_by_id(
|
|
204
|
+
client,
|
|
205
|
+
request_id=request_id,
|
|
206
|
+
log_type=type_int,
|
|
207
|
+
model_name=model,
|
|
208
|
+
token_name=token_name,
|
|
209
|
+
start_timestamp=start_ts,
|
|
210
|
+
end_timestamp=end_ts,
|
|
211
|
+
group=group,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if log_entry is None:
|
|
215
|
+
err_console.print(
|
|
216
|
+
f"[red]No log found for request_id=[/]{request_id}\n"
|
|
217
|
+
"[dim]The entry may be older than the search window. "
|
|
218
|
+
"Try narrowing with --start/--end or --model.[/]"
|
|
219
|
+
)
|
|
220
|
+
raise typer.Exit(code=1)
|
|
221
|
+
|
|
222
|
+
if fmt == OutputFormat.JSON:
|
|
223
|
+
output(log_entry, fmt)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
info = _format_log_detail(log_entry)
|
|
227
|
+
output(info, fmt, title="Request Detail")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Server-side CSV export — write raw bytes to stdout and return early
|
|
231
|
+
if export:
|
|
232
|
+
type_int = None
|
|
233
|
+
if log_type:
|
|
234
|
+
type_int = LOG_TYPE_MAP.get(log_type.lower())
|
|
235
|
+
if type_int is None:
|
|
236
|
+
valid = ", ".join(LOG_TYPE_MAP.keys())
|
|
237
|
+
err_console.print(f"[red]Invalid log type:[/] {log_type}. Valid types: {valid}")
|
|
238
|
+
raise typer.Exit(code=2)
|
|
239
|
+
csv_bytes = client.export_logs(
|
|
240
|
+
log_type=type_int,
|
|
241
|
+
model_name=model,
|
|
242
|
+
token_name=token_name,
|
|
243
|
+
start_timestamp=start_ts,
|
|
244
|
+
end_timestamp=end_ts,
|
|
245
|
+
group=group,
|
|
246
|
+
)
|
|
247
|
+
sys.stdout.buffer.write(csv_bytes)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if search:
|
|
251
|
+
err_console.print(
|
|
252
|
+
"[yellow]Warning:[/] --search only matches log type codes on the server, "
|
|
253
|
+
"not keywords. Use --request-id, --model, or --token-name for filtering."
|
|
254
|
+
)
|
|
255
|
+
ignored = []
|
|
256
|
+
if model:
|
|
257
|
+
ignored.append("--model")
|
|
258
|
+
if token_name:
|
|
259
|
+
ignored.append("--token-name")
|
|
260
|
+
if log_type:
|
|
261
|
+
ignored.append("--type")
|
|
262
|
+
if start:
|
|
263
|
+
ignored.append("--start")
|
|
264
|
+
if end:
|
|
265
|
+
ignored.append("--end")
|
|
266
|
+
if group:
|
|
267
|
+
ignored.append("--group")
|
|
268
|
+
if page != 1:
|
|
269
|
+
ignored.append("--page")
|
|
270
|
+
if limit != 20:
|
|
271
|
+
ignored.append("--limit")
|
|
272
|
+
if ignored:
|
|
273
|
+
err_console.print(
|
|
274
|
+
f"[yellow]Warning:[/] --search ignores other filters: {', '.join(ignored)}"
|
|
275
|
+
)
|
|
276
|
+
resp = client.search_logs(keyword=search)
|
|
277
|
+
else:
|
|
278
|
+
type_int = None
|
|
279
|
+
if log_type:
|
|
280
|
+
type_int = LOG_TYPE_MAP.get(log_type.lower())
|
|
281
|
+
if type_int is None:
|
|
282
|
+
valid = ", ".join(LOG_TYPE_MAP.keys())
|
|
283
|
+
err_console.print(f"[red]Invalid log type:[/] {log_type}. Valid types: {valid}")
|
|
284
|
+
raise typer.Exit(code=2)
|
|
285
|
+
|
|
286
|
+
resp = client.list_logs(
|
|
287
|
+
page=page,
|
|
288
|
+
page_size=limit,
|
|
289
|
+
log_type=type_int,
|
|
290
|
+
model_name=model,
|
|
291
|
+
token_name=token_name,
|
|
292
|
+
start_timestamp=start_ts,
|
|
293
|
+
end_timestamp=end_ts,
|
|
294
|
+
group=group,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
data = extract_items(resp)
|
|
298
|
+
|
|
299
|
+
if fmt == OutputFormat.JSON:
|
|
300
|
+
output(data, fmt)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
if not data:
|
|
304
|
+
console.print("[dim]No logs found.[/]")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
rows = [_format_log_row(log, detail=detail) for log in data]
|
|
308
|
+
columns: dict[str, str] = {
|
|
309
|
+
"time": "dim",
|
|
310
|
+
"type": "cyan",
|
|
311
|
+
"model": "green",
|
|
312
|
+
"token": "yellow",
|
|
313
|
+
"prompt": "green",
|
|
314
|
+
"completion": "green",
|
|
315
|
+
"cost": "red",
|
|
316
|
+
"duration_ms": "dim",
|
|
317
|
+
"stream": "dim",
|
|
318
|
+
}
|
|
319
|
+
if detail:
|
|
320
|
+
columns["request_id"] = "blue"
|
|
321
|
+
columns["response_id"] = "blue"
|
|
322
|
+
columns["model_ratio"] = "magenta"
|
|
323
|
+
columns["group_ratio"] = "magenta"
|
|
324
|
+
columns["completion_ratio"] = "magenta"
|
|
325
|
+
columns["model_price"] = "magenta"
|
|
326
|
+
output(rows, fmt, title="Usage Logs", columns=columns)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Models command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import get_client
|
|
10
|
+
from ..errors import handle_errors
|
|
11
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@handle_errors
|
|
15
|
+
def models(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
search: Annotated[str | None, typer.Option("--search", "-s", help="Filter models by name.")] = None,
|
|
18
|
+
limit: Annotated[int | None, typer.Option("--limit", "-l", help="Max number of models to show.")] = None,
|
|
19
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
20
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""List available models."""
|
|
23
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
24
|
+
client = get_client()
|
|
25
|
+
result = client.models.list()
|
|
26
|
+
|
|
27
|
+
rows = sorted(
|
|
28
|
+
[{"id": m.id, "owned_by": m.owned_by} for m in result],
|
|
29
|
+
key=lambda r: r["id"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if search:
|
|
33
|
+
term = search.lower()
|
|
34
|
+
rows = [r for r in rows if term in r["id"].lower()]
|
|
35
|
+
|
|
36
|
+
if limit and limit > 0:
|
|
37
|
+
rows = rows[:limit]
|
|
38
|
+
|
|
39
|
+
output(
|
|
40
|
+
rows,
|
|
41
|
+
fmt,
|
|
42
|
+
title=f"Available Models ({len(rows)})",
|
|
43
|
+
columns={"id": "cyan", "owned_by": "green"},
|
|
44
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Interactive command REPL for CometAPI CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit import PromptSession
|
|
8
|
+
from prompt_toolkit.completion import WordCompleter
|
|
9
|
+
from prompt_toolkit.formatted_text import HTML
|
|
10
|
+
from prompt_toolkit.history import FileHistory
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from ..config import CONFIG_DIR, get_value, mask_secret
|
|
15
|
+
from ..console import console
|
|
16
|
+
from ..errors import handle_errors
|
|
17
|
+
|
|
18
|
+
REPL_COMMANDS = [
|
|
19
|
+
"chat", "models", "balance", "account", "stats",
|
|
20
|
+
"tokens", "logs", "tasks",
|
|
21
|
+
"doctor", "init", "config", "help", "exit", "quit",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _repl_banner() -> Panel:
|
|
26
|
+
from cometapi_cli import __version__
|
|
27
|
+
|
|
28
|
+
api_key = get_value("api_key", "COMETAPI_KEY")
|
|
29
|
+
key_display = mask_secret(api_key) if api_key else "[not set]"
|
|
30
|
+
|
|
31
|
+
text = Text.assemble(
|
|
32
|
+
("CometAPI REPL", "bold cyan"),
|
|
33
|
+
(f" v{__version__}", "dim"),
|
|
34
|
+
"\n",
|
|
35
|
+
("API Key: ", "dim"),
|
|
36
|
+
(key_display, "green" if api_key else "red"),
|
|
37
|
+
"\n",
|
|
38
|
+
("Type any command without 'cometapi' prefix. 'help' for commands, 'exit' to quit.", "dim"),
|
|
39
|
+
)
|
|
40
|
+
return Panel(text, border_style="cyan", padding=(0, 1))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _show_help() -> None:
|
|
44
|
+
console.print("\n[bold]Available commands:[/]")
|
|
45
|
+
console.print(" [cyan]chat [message][/] Send a message or start chat REPL")
|
|
46
|
+
console.print(" [cyan]models[/] List available models")
|
|
47
|
+
console.print(" [cyan]balance[/] Show account balance")
|
|
48
|
+
console.print(" [cyan]account[/] Show account info")
|
|
49
|
+
console.print(" [cyan]stats[/] Show usage statistics")
|
|
50
|
+
console.print(" [cyan]tokens[/] List your API keys")
|
|
51
|
+
console.print(" [cyan]logs[/] Show usage logs")
|
|
52
|
+
console.print(" [cyan]tasks[/] Show async task logs")
|
|
53
|
+
console.print(" [cyan]doctor[/] Run diagnostics")
|
|
54
|
+
console.print(" [cyan]init[/] Run setup wizard")
|
|
55
|
+
console.print(" [cyan]config ...[/] Manage configuration")
|
|
56
|
+
console.print(" [cyan]help[/] Show this help")
|
|
57
|
+
console.print(" [cyan]exit[/] Exit REPL")
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@handle_errors
|
|
62
|
+
def run_repl() -> None:
|
|
63
|
+
"""Start interactive command REPL."""
|
|
64
|
+
import sys
|
|
65
|
+
|
|
66
|
+
console.print(_repl_banner())
|
|
67
|
+
|
|
68
|
+
history_file = CONFIG_DIR / "repl_history"
|
|
69
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
completer = WordCompleter(
|
|
72
|
+
REPL_COMMANDS + [
|
|
73
|
+
"--json", "--format", "--model", "--system",
|
|
74
|
+
"--search", "--limit", "--help", "--page",
|
|
75
|
+
"--type", "--token-name", "--start", "--end",
|
|
76
|
+
"--group", "--export", "--platform", "--task-id",
|
|
77
|
+
"--status", "--action",
|
|
78
|
+
],
|
|
79
|
+
ignore_case=True,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
session: PromptSession[str] = PromptSession(
|
|
83
|
+
history=FileHistory(str(history_file)),
|
|
84
|
+
completer=completer,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
user_input = session.prompt(HTML("<b><ansicyan>cometapi → </ansicyan></b>")).strip()
|
|
90
|
+
except (EOFError, KeyboardInterrupt):
|
|
91
|
+
console.print("\n[dim]Goodbye![/]")
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if not user_input:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if user_input.lower() in ("exit", "quit", "q"):
|
|
98
|
+
console.print("[dim]Goodbye![/]")
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
if user_input.lower() == "help":
|
|
102
|
+
_show_help()
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Parse the input and dispatch through the Typer app
|
|
106
|
+
try:
|
|
107
|
+
args = shlex.split(user_input)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
console.print(f"[red]Parse error: {e}[/]")
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Strip leading "cometapi" if user types it by habit
|
|
113
|
+
if args and args[0] == "cometapi":
|
|
114
|
+
args = args[1:]
|
|
115
|
+
|
|
116
|
+
if not args:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Prevent recursive REPL
|
|
120
|
+
if args[0] == "repl":
|
|
121
|
+
console.print("[yellow]Already in REPL mode.[/]")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
from ..app import app
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
app(args, standalone_mode=False)
|
|
128
|
+
except SystemExit:
|
|
129
|
+
pass
|
|
130
|
+
except Exception as e:
|
|
131
|
+
console.print(f"[red bold]Error:[/] {e}")
|
|
132
|
+
|
|
133
|
+
# Reset stdout in case streaming left it in a bad state
|
|
134
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Stats command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import get_client
|
|
10
|
+
from ..errors import handle_errors
|
|
11
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@handle_errors
|
|
15
|
+
def stats(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
18
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Show your CometAPI usage statistics (requires access token)."""
|
|
21
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
22
|
+
client = get_client(require_access_token=True)
|
|
23
|
+
resp = client.get_user_stats()
|
|
24
|
+
data = resp.get("data", {})
|
|
25
|
+
|
|
26
|
+
display = {
|
|
27
|
+
"requests": data.get("month_req_count", "N/A"),
|
|
28
|
+
"request_rate_change": f"{data.get('req_rate', 0):.1f}%",
|
|
29
|
+
"usage": f"${data.get('month_usage', 0):.4f}",
|
|
30
|
+
"usage_rate_change": f"{data.get('usage_rate', 0):.1f}%",
|
|
31
|
+
"success_rate": f"{data.get('month_success_rate', 0):.1f}%",
|
|
32
|
+
"predicted_days_left": data.get("predicted_days", "N/A"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# For JSON output, pass raw data for maximum information
|
|
36
|
+
if fmt == OutputFormat.JSON:
|
|
37
|
+
output(data, fmt)
|
|
38
|
+
else:
|
|
39
|
+
output(display, fmt, title="CometAPI Usage Statistics (This Month)")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Task logs command — async tasks (Suno, Midjourney, Luma, Kling, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import get_client
|
|
10
|
+
from ..constants import extract_items, format_ts, parse_date, quota_to_usd
|
|
11
|
+
from ..errors import handle_errors
|
|
12
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
13
|
+
|
|
14
|
+
VALID_PLATFORMS = [
|
|
15
|
+
"suno", "mj", "luma", "replicate", "kling",
|
|
16
|
+
"runwayml", "runway", "jimeng", "volcengine", "sora", "bria", "flux",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
VALID_STATUSES = [
|
|
20
|
+
"SUBMITTED", "QUEUED", "IN_PROGRESS", "FAILURE", "SUCCESS", "UNKNOWN",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _duration(submit_ts: int, finish_ts: int) -> str:
|
|
25
|
+
"""Calculate human-readable duration between two timestamps."""
|
|
26
|
+
if not submit_ts or not finish_ts:
|
|
27
|
+
return "—"
|
|
28
|
+
secs = finish_ts - submit_ts
|
|
29
|
+
if secs < 0:
|
|
30
|
+
return "—"
|
|
31
|
+
if secs < 60:
|
|
32
|
+
return f"{secs}s"
|
|
33
|
+
mins = secs // 60
|
|
34
|
+
remaining = secs % 60
|
|
35
|
+
return f"{mins}m{remaining}s"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_task_row(task: dict) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"time": format_ts(task.get("created_at", 0)),
|
|
41
|
+
"platform": task.get("platform", "") or "—",
|
|
42
|
+
"task_id": task.get("task_id", "") or "—",
|
|
43
|
+
"action": task.get("action", "") or "—",
|
|
44
|
+
"status": task.get("status", "") or "—",
|
|
45
|
+
"model": task.get("model_name", "") or "—",
|
|
46
|
+
"progress": task.get("progress", "") or "—",
|
|
47
|
+
"cost": quota_to_usd(task.get("quota", 0)),
|
|
48
|
+
"duration": _duration(task.get("submit_time", 0), task.get("finish_time", 0)),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@handle_errors
|
|
53
|
+
def tasks(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
platform: Annotated[
|
|
56
|
+
str | None,
|
|
57
|
+
typer.Option("--platform", "-p", help=f"Filter by platform: {', '.join(VALID_PLATFORMS)}."),
|
|
58
|
+
] = None,
|
|
59
|
+
task_id: Annotated[str | None, typer.Option("--task-id", help="Filter by task ID.")] = None,
|
|
60
|
+
status: Annotated[
|
|
61
|
+
str | None,
|
|
62
|
+
typer.Option("--status", "-s", help=f"Filter by status: {', '.join(VALID_STATUSES)}."),
|
|
63
|
+
] = None,
|
|
64
|
+
action: Annotated[str | None, typer.Option("--action", "-a", help="Filter by action type.")] = None,
|
|
65
|
+
start: Annotated[str | None, typer.Option("--start", help="Start date (YYYY-MM-DD, ISO 8601, or Unix ts).")] = None,
|
|
66
|
+
end: Annotated[str | None, typer.Option("--end", help="End date (YYYY-MM-DD, ISO 8601, or Unix ts).")] = None,
|
|
67
|
+
page: Annotated[int, typer.Option("--page", help="Page number.")] = 1,
|
|
68
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Results per page.")] = 20,
|
|
69
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
70
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Show your async task logs — Suno, MJ, Luma, Kling, etc. (requires access token)."""
|
|
73
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
74
|
+
client = get_client(require_access_token=True)
|
|
75
|
+
|
|
76
|
+
# Validate platform
|
|
77
|
+
if platform and platform.lower() not in VALID_PLATFORMS:
|
|
78
|
+
from ..console import err_console
|
|
79
|
+
|
|
80
|
+
valid = ", ".join(VALID_PLATFORMS)
|
|
81
|
+
err_console.print(f"[red]Invalid platform:[/] {platform}. Valid platforms: {valid}")
|
|
82
|
+
raise typer.Exit(code=2)
|
|
83
|
+
|
|
84
|
+
# Validate status
|
|
85
|
+
if status and status.upper() not in VALID_STATUSES:
|
|
86
|
+
from ..console import err_console
|
|
87
|
+
|
|
88
|
+
valid = ", ".join(VALID_STATUSES)
|
|
89
|
+
err_console.print(f"[red]Invalid status:[/] {status}. Valid statuses: {valid}")
|
|
90
|
+
raise typer.Exit(code=2)
|
|
91
|
+
|
|
92
|
+
start_ts = parse_date(start, "start") if start else None
|
|
93
|
+
end_ts = parse_date(end, "end") if end else None
|
|
94
|
+
|
|
95
|
+
resp = client.list_tasks(
|
|
96
|
+
page=page,
|
|
97
|
+
page_size=limit,
|
|
98
|
+
platform=platform.lower() if platform else None,
|
|
99
|
+
task_id=task_id,
|
|
100
|
+
status=status.upper() if status else None,
|
|
101
|
+
action=action,
|
|
102
|
+
start_timestamp=start_ts,
|
|
103
|
+
end_timestamp=end_ts,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
data = extract_items(resp)
|
|
107
|
+
|
|
108
|
+
if fmt == OutputFormat.JSON:
|
|
109
|
+
output(data, fmt)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if not data:
|
|
113
|
+
from ..console import console
|
|
114
|
+
|
|
115
|
+
console.print("[dim]No tasks found.[/]")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
rows = [_format_task_row(t) for t in data]
|
|
119
|
+
columns = {
|
|
120
|
+
"time": "dim",
|
|
121
|
+
"platform": "magenta",
|
|
122
|
+
"task_id": "cyan",
|
|
123
|
+
"action": "yellow",
|
|
124
|
+
"status": "green",
|
|
125
|
+
"model": "green",
|
|
126
|
+
"progress": "dim",
|
|
127
|
+
"cost": "red",
|
|
128
|
+
"duration": "dim",
|
|
129
|
+
}
|
|
130
|
+
output(rows, fmt, title="Task Logs", columns=columns)
|