cometapi-cli 0.3.0__py3-none-any.whl → 0.3.2__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 +1 -1
- cometapi_cli/client.py +15 -1
- cometapi_cli/commands/logs.py +203 -67
- cometapi_cli/constants.py +7 -0
- {cometapi_cli-0.3.0.dist-info → cometapi_cli-0.3.2.dist-info}/METADATA +17 -1
- {cometapi_cli-0.3.0.dist-info → cometapi_cli-0.3.2.dist-info}/RECORD +9 -9
- {cometapi_cli-0.3.0.dist-info → cometapi_cli-0.3.2.dist-info}/WHEEL +1 -1
- {cometapi_cli-0.3.0.dist-info → cometapi_cli-0.3.2.dist-info}/entry_points.txt +0 -0
- {cometapi_cli-0.3.0.dist-info → cometapi_cli-0.3.2.dist-info}/licenses/LICENSE +0 -0
cometapi_cli/__init__.py
CHANGED
cometapi_cli/client.py
CHANGED
|
@@ -9,6 +9,8 @@ import openai
|
|
|
9
9
|
|
|
10
10
|
COMETAPI_BASE_URL = "https://api.cometapi.com/v1"
|
|
11
11
|
COMETAPI_DASHBOARD_BASE = "https://api.cometapi.com"
|
|
12
|
+
ACCOUNT_REQUEST_TIMEOUT = 30.0
|
|
13
|
+
ACCOUNT_EXPORT_TIMEOUT = 120.0
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CometClient(openai.OpenAI):
|
|
@@ -44,7 +46,14 @@ class CometClient(openai.OpenAI):
|
|
|
44
46
|
|
|
45
47
|
# -- Account management (access-token auth) --------------------------------
|
|
46
48
|
|
|
47
|
-
def _account_request(
|
|
49
|
+
def _account_request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
path: str,
|
|
53
|
+
*,
|
|
54
|
+
params: dict | None = None,
|
|
55
|
+
timeout: float = ACCOUNT_REQUEST_TIMEOUT,
|
|
56
|
+
) -> dict:
|
|
48
57
|
"""Make an authenticated request to a CometAPI account endpoint."""
|
|
49
58
|
if not self._access_token:
|
|
50
59
|
raise openai.OpenAIError(
|
|
@@ -56,6 +65,7 @@ class CometClient(openai.OpenAI):
|
|
|
56
65
|
f"{COMETAPI_DASHBOARD_BASE}{path}",
|
|
57
66
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
58
67
|
params=params,
|
|
68
|
+
timeout=timeout,
|
|
59
69
|
)
|
|
60
70
|
response.raise_for_status()
|
|
61
71
|
return response.json()
|
|
@@ -151,6 +161,7 @@ class CometClient(openai.OpenAI):
|
|
|
151
161
|
start_timestamp: int | None = None,
|
|
152
162
|
end_timestamp: int | None = None,
|
|
153
163
|
group: str | None = None,
|
|
164
|
+
request_id: str | None = None,
|
|
154
165
|
) -> dict:
|
|
155
166
|
"""List the user's usage logs (requires access token)."""
|
|
156
167
|
params: dict[str, Any] = {"p": page, "page_size": page_size}
|
|
@@ -166,6 +177,8 @@ class CometClient(openai.OpenAI):
|
|
|
166
177
|
params["end_timestamp"] = end_timestamp
|
|
167
178
|
if group:
|
|
168
179
|
params["group"] = group
|
|
180
|
+
if request_id:
|
|
181
|
+
params["request_id"] = request_id
|
|
169
182
|
return self._account_request("GET", "/api/log/self", params=params)
|
|
170
183
|
|
|
171
184
|
def search_logs(self, keyword: str) -> dict:
|
|
@@ -235,6 +248,7 @@ class CometClient(openai.OpenAI):
|
|
|
235
248
|
f"{COMETAPI_DASHBOARD_BASE}/api/log/self/export",
|
|
236
249
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
237
250
|
params=params,
|
|
251
|
+
timeout=ACCOUNT_EXPORT_TIMEOUT,
|
|
238
252
|
)
|
|
239
253
|
response.raise_for_status()
|
|
240
254
|
return response.content
|
cometapi_cli/commands/logs.py
CHANGED
|
@@ -4,12 +4,20 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json as _json
|
|
6
6
|
import sys
|
|
7
|
+
from datetime import datetime, time, timedelta, timezone
|
|
7
8
|
from typing import Annotated
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
|
|
11
12
|
from ..config import get_client
|
|
12
|
-
from ..constants import
|
|
13
|
+
from ..constants import (
|
|
14
|
+
QUOTA_PER_UNIT,
|
|
15
|
+
extract_items,
|
|
16
|
+
format_iso,
|
|
17
|
+
format_ts,
|
|
18
|
+
parse_date,
|
|
19
|
+
quota_to_usd,
|
|
20
|
+
)
|
|
13
21
|
from ..errors import handle_errors
|
|
14
22
|
from ..formatters import OutputFormat, output, resolve_format
|
|
15
23
|
|
|
@@ -24,6 +32,9 @@ LOG_TYPE_MAP = {
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
_LOG_TYPE_NAMES = {v: k for k, v in LOG_TYPE_MAP.items()}
|
|
35
|
+
REQUEST_ID_PAGE_SIZE = 100
|
|
36
|
+
REQUEST_ID_DEFAULT_MAX_PAGES = 10
|
|
37
|
+
REQUEST_ID_LOCAL_TZ = timezone(timedelta(hours=8))
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
def _parse_other(other_raw: str | None) -> dict:
|
|
@@ -37,7 +48,29 @@ def _parse_other(other_raw: str | None) -> dict:
|
|
|
37
48
|
return {}
|
|
38
49
|
|
|
39
50
|
|
|
40
|
-
def
|
|
51
|
+
def _request_id_date_window(request_id: str) -> tuple[int, int] | None:
|
|
52
|
+
"""Infer the local request day from a CometAPI request ID prefix."""
|
|
53
|
+
prefix = request_id[:14]
|
|
54
|
+
if len(prefix) != 14 or not prefix.isdigit():
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
local_dt = datetime.strptime(prefix, "%Y%m%d%H%M%S").replace(tzinfo=REQUEST_ID_LOCAL_TZ)
|
|
58
|
+
except ValueError:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
local_day_start = datetime.combine(local_dt.date(), time.min, tzinfo=REQUEST_ID_LOCAL_TZ)
|
|
62
|
+
local_day_end = local_day_start + timedelta(days=1) - timedelta(seconds=1)
|
|
63
|
+
return int(local_day_start.timestamp()), int(local_day_end.timestamp())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _exact_request_id_match(items: list, request_id: str) -> dict | None:
|
|
67
|
+
for item in items:
|
|
68
|
+
if isinstance(item, dict) and item.get("request_id") == request_id:
|
|
69
|
+
return item
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _lookup_log_by_id(
|
|
41
74
|
client: object,
|
|
42
75
|
*,
|
|
43
76
|
request_id: str,
|
|
@@ -47,84 +80,166 @@ def _find_log_by_id(
|
|
|
47
80
|
start_timestamp: int | None = None,
|
|
48
81
|
end_timestamp: int | None = None,
|
|
49
82
|
group: str | None = None,
|
|
50
|
-
max_pages: int =
|
|
51
|
-
) -> dict | None:
|
|
52
|
-
"""
|
|
83
|
+
max_pages: int = REQUEST_ID_DEFAULT_MAX_PAGES,
|
|
84
|
+
) -> tuple[dict | None, dict]:
|
|
85
|
+
"""Look up a log entry by request ID, using server filtering before fallback scans."""
|
|
86
|
+
meta = {
|
|
87
|
+
"used_server_filter": False,
|
|
88
|
+
"used_inferred_window": False,
|
|
89
|
+
"scanned_pages": 0,
|
|
90
|
+
"max_pages": max_pages,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Older backends may ignore request_id, so verify the returned item matches
|
|
94
|
+
# before trusting this direct lookup.
|
|
95
|
+
resp = client.list_logs( # type: ignore[union-attr]
|
|
96
|
+
page=1,
|
|
97
|
+
page_size=REQUEST_ID_PAGE_SIZE,
|
|
98
|
+
log_type=log_type,
|
|
99
|
+
model_name=model_name,
|
|
100
|
+
token_name=token_name,
|
|
101
|
+
start_timestamp=start_timestamp,
|
|
102
|
+
end_timestamp=end_timestamp,
|
|
103
|
+
group=group,
|
|
104
|
+
request_id=request_id,
|
|
105
|
+
)
|
|
106
|
+
meta["used_server_filter"] = True
|
|
107
|
+
if match := _exact_request_id_match(extract_items(resp), request_id):
|
|
108
|
+
return match, meta
|
|
109
|
+
|
|
110
|
+
scan_start = start_timestamp
|
|
111
|
+
scan_end = end_timestamp
|
|
112
|
+
if scan_start is None and scan_end is None:
|
|
113
|
+
inferred = _request_id_date_window(request_id)
|
|
114
|
+
if inferred:
|
|
115
|
+
scan_start, scan_end = inferred
|
|
116
|
+
meta["used_inferred_window"] = True
|
|
117
|
+
|
|
53
118
|
for pg in range(1, max_pages + 1):
|
|
54
119
|
resp = client.list_logs( # type: ignore[union-attr]
|
|
55
120
|
page=pg,
|
|
56
|
-
page_size=
|
|
121
|
+
page_size=REQUEST_ID_PAGE_SIZE,
|
|
57
122
|
log_type=log_type,
|
|
58
123
|
model_name=model_name,
|
|
59
124
|
token_name=token_name,
|
|
60
|
-
start_timestamp=
|
|
61
|
-
end_timestamp=
|
|
125
|
+
start_timestamp=scan_start,
|
|
126
|
+
end_timestamp=scan_end,
|
|
62
127
|
group=group,
|
|
63
128
|
)
|
|
129
|
+
meta["scanned_pages"] = pg
|
|
64
130
|
items = extract_items(resp)
|
|
65
131
|
if not items:
|
|
66
132
|
break
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
133
|
+
if match := _exact_request_id_match(items, request_id):
|
|
134
|
+
return match, meta
|
|
135
|
+
return None, meta
|
|
136
|
+
|
|
71
137
|
|
|
138
|
+
def _build_log_record(log: dict) -> dict:
|
|
139
|
+
"""Build the canonical, machine-friendly record for a single log entry.
|
|
72
140
|
|
|
73
|
-
|
|
74
|
-
|
|
141
|
+
Returns a stable schema with raw, typed values (numbers, booleans, ISO 8601
|
|
142
|
+
time). The same field set drives every output format; human-facing formats
|
|
143
|
+
layer display formatting on top via :func:`_record_to_detail_display`.
|
|
144
|
+
Missing values are ``None`` so the schema stays consistent across entries.
|
|
145
|
+
"""
|
|
75
146
|
other = _parse_other(log.get("other"))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
147
|
+
quota = log.get("quota", 0) or 0
|
|
148
|
+
|
|
149
|
+
# model_price == -1 is the backend sentinel for "use ratio-based pricing"
|
|
150
|
+
# (i.e. no fixed per-call price); expose it as null in the canonical record.
|
|
151
|
+
raw_model_price = other.get("model_price")
|
|
152
|
+
model_price = None if raw_model_price == -1 else raw_model_price
|
|
153
|
+
|
|
154
|
+
# Prefer total_ms (true end-to-end latency) over use_time, which the backend
|
|
155
|
+
# records in seconds and is too coarse to be useful as a duration.
|
|
156
|
+
total_ms = other.get("total_ms")
|
|
157
|
+
use_time = log.get("use_time", 0) or 0
|
|
158
|
+
duration_ms = total_ms if total_ms else (use_time or None)
|
|
159
|
+
|
|
160
|
+
frt = other.get("frt")
|
|
161
|
+
user_id = log.get("user_id")
|
|
79
162
|
|
|
163
|
+
return {
|
|
164
|
+
"request_id": log.get("request_id") or None,
|
|
165
|
+
"response_id": log.get("response_id") or None,
|
|
166
|
+
"time": format_iso(log.get("created_at", 0)),
|
|
167
|
+
"type": _LOG_TYPE_NAMES.get(log.get("type", 0), "unknown"),
|
|
168
|
+
"model": log.get("model_name") or None,
|
|
169
|
+
"token_name": log.get("token_name") or None,
|
|
170
|
+
"username": log.get("username") or None,
|
|
171
|
+
"user_id": user_id if user_id else None,
|
|
172
|
+
"ip": log.get("ip") or None,
|
|
173
|
+
"stream": bool(log.get("is_stream")),
|
|
174
|
+
"prompt_tokens": log.get("prompt_tokens", 0) or 0,
|
|
175
|
+
"completion_tokens": log.get("completion_tokens", 0) or 0,
|
|
176
|
+
"cache_tokens": other.get("cache_tokens", 0) or 0,
|
|
177
|
+
"cost_usd": round(quota / QUOTA_PER_UNIT, 6),
|
|
178
|
+
"quota": quota,
|
|
179
|
+
"model_ratio": other.get("model_ratio"),
|
|
180
|
+
"completion_ratio": other.get("completion_ratio"),
|
|
181
|
+
"group_ratio": other.get("group_ratio"),
|
|
182
|
+
"model_price": model_price,
|
|
183
|
+
"cache_ratio": other.get("cache_ratio"),
|
|
184
|
+
"duration_ms": duration_ms,
|
|
185
|
+
"first_token_ms": frt if frt and frt > 0 else None,
|
|
186
|
+
"endpoint": other.get("request_path") or None,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _record_to_detail_display(rec: dict) -> dict:
|
|
191
|
+
"""Render a canonical log record as a formatted key-value detail card.
|
|
192
|
+
|
|
193
|
+
Mirrors the field set of :func:`_build_log_record` but uses human-friendly
|
|
194
|
+
labels and formatted strings (USD, ``Yes``/``No``, ``ms``). Optional fields
|
|
195
|
+
are omitted when absent to keep the card readable.
|
|
196
|
+
"""
|
|
197
|
+
em_dash = "—"
|
|
80
198
|
info: dict[str, str | int | float] = {}
|
|
81
|
-
info["Request ID"] =
|
|
82
|
-
info["Response ID"] =
|
|
83
|
-
|
|
84
|
-
info["
|
|
85
|
-
info["
|
|
86
|
-
info["
|
|
87
|
-
|
|
199
|
+
info["Request ID"] = rec["request_id"] or em_dash
|
|
200
|
+
info["Response ID"] = rec["response_id"] or em_dash
|
|
201
|
+
iso = rec["time"]
|
|
202
|
+
info["Time"] = iso[:19].replace("T", " ") if iso else em_dash
|
|
203
|
+
info["Model"] = rec["model"] or em_dash
|
|
204
|
+
info["Token Name"] = rec["token_name"] or em_dash
|
|
205
|
+
if rec["username"]:
|
|
206
|
+
info["Username"] = rec["username"]
|
|
207
|
+
if rec["user_id"] is not None:
|
|
208
|
+
info["User ID"] = rec["user_id"]
|
|
209
|
+
if rec["ip"]:
|
|
210
|
+
info["IP"] = rec["ip"]
|
|
211
|
+
info["Type"] = rec["type"]
|
|
212
|
+
info["Stream"] = "Yes" if rec["stream"] else "No"
|
|
88
213
|
|
|
89
214
|
# Tokens
|
|
90
|
-
info["Prompt Tokens"] = f"{prompt_tokens:,}"
|
|
91
|
-
info["Completion Tokens"] = f"{completion_tokens:,}"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
info["
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
info["
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
215
|
+
info["Prompt Tokens"] = f"{rec['prompt_tokens']:,}"
|
|
216
|
+
info["Completion Tokens"] = f"{rec['completion_tokens']:,}"
|
|
217
|
+
if rec["cache_tokens"]:
|
|
218
|
+
info["Cache Tokens"] = f"{rec['cache_tokens']:,}"
|
|
219
|
+
|
|
220
|
+
# Cost (6 decimals so sub-cent calls are not rounded away)
|
|
221
|
+
info["Cost (USD)"] = f"${rec['cost_usd']:,.6f}"
|
|
222
|
+
info["Quota (raw)"] = f"{rec['quota']:,}"
|
|
223
|
+
|
|
224
|
+
# Pricing breakdown
|
|
225
|
+
if rec["model_ratio"] is not None:
|
|
226
|
+
info["Model Ratio"] = rec["model_ratio"]
|
|
227
|
+
if rec["completion_ratio"] is not None:
|
|
228
|
+
info["Completion Ratio"] = f"{rec['completion_ratio']}x"
|
|
229
|
+
if rec["group_ratio"] is not None:
|
|
230
|
+
info["Group Ratio"] = rec["group_ratio"]
|
|
231
|
+
info["Model Price"] = "default" if rec["model_price"] is None else rec["model_price"]
|
|
232
|
+
if rec["cache_ratio"] is not None:
|
|
233
|
+
info["Cache Ratio"] = rec["cache_ratio"]
|
|
116
234
|
|
|
117
235
|
# Timing
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if frt and frt > 0:
|
|
122
|
-
info["First Token"] = f"{frt:,} ms"
|
|
236
|
+
info["Duration"] = f"{rec['duration_ms']:,} ms" if rec["duration_ms"] else em_dash
|
|
237
|
+
if rec["first_token_ms"]:
|
|
238
|
+
info["First Token"] = f"{rec['first_token_ms']:,} ms"
|
|
123
239
|
|
|
124
240
|
# Path
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
info["Endpoint"] = request_path
|
|
241
|
+
if rec["endpoint"]:
|
|
242
|
+
info["Endpoint"] = rec["endpoint"]
|
|
128
243
|
|
|
129
244
|
return info
|
|
130
245
|
|
|
@@ -169,6 +284,14 @@ def logs(
|
|
|
169
284
|
str | None,
|
|
170
285
|
typer.Option("--request-id", help="Look up cost by request ID (X-Cometapi-Request-Id header)."),
|
|
171
286
|
] = None,
|
|
287
|
+
request_id_max_pages: Annotated[
|
|
288
|
+
int,
|
|
289
|
+
typer.Option(
|
|
290
|
+
"--request-id-max-pages",
|
|
291
|
+
min=1,
|
|
292
|
+
help="Fallback pages to scan when direct request-ID lookup is unavailable.",
|
|
293
|
+
),
|
|
294
|
+
] = REQUEST_ID_DEFAULT_MAX_PAGES,
|
|
172
295
|
detail: Annotated[
|
|
173
296
|
bool,
|
|
174
297
|
typer.Option("--detail", help="Show extended columns (request ID, pricing ratios)."),
|
|
@@ -202,7 +325,7 @@ def logs(
|
|
|
202
325
|
if log_type:
|
|
203
326
|
type_int = LOG_TYPE_MAP.get(log_type.lower())
|
|
204
327
|
|
|
205
|
-
log_entry =
|
|
328
|
+
log_entry, lookup_meta = _lookup_log_by_id(
|
|
206
329
|
client,
|
|
207
330
|
request_id=request_id,
|
|
208
331
|
log_type=type_int,
|
|
@@ -211,22 +334,32 @@ def logs(
|
|
|
211
334
|
start_timestamp=start_ts,
|
|
212
335
|
end_timestamp=end_ts,
|
|
213
336
|
group=group,
|
|
337
|
+
max_pages=request_id_max_pages,
|
|
214
338
|
)
|
|
215
339
|
|
|
216
340
|
if log_entry is None:
|
|
341
|
+
if lookup_meta["used_inferred_window"]:
|
|
342
|
+
scan_note = (
|
|
343
|
+
f"Then scanned {lookup_meta['scanned_pages']} page(s) in the request ID's "
|
|
344
|
+
"inferred local date window."
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
scan_note = f"Then scanned {lookup_meta['scanned_pages']} fallback page(s)."
|
|
217
348
|
err_console.print(
|
|
218
349
|
f"[red]No log found for request_id=[/]{request_id}\n"
|
|
219
|
-
"[dim]
|
|
220
|
-
"
|
|
350
|
+
"[dim]Tried direct request-id lookup first. "
|
|
351
|
+
f"{scan_note} Increase --request-id-max-pages or narrow with --start/--end "
|
|
352
|
+
"if the backend does not support direct request-id filtering.[/]"
|
|
221
353
|
)
|
|
222
354
|
raise typer.Exit(code=1)
|
|
223
355
|
|
|
224
|
-
|
|
225
|
-
|
|
356
|
+
record = _build_log_record(log_entry)
|
|
357
|
+
# Machine formats get raw typed values; human formats get the formatted card.
|
|
358
|
+
if fmt in (OutputFormat.JSON, OutputFormat.YAML, OutputFormat.CSV):
|
|
359
|
+
output(record, fmt)
|
|
226
360
|
return
|
|
227
361
|
|
|
228
|
-
|
|
229
|
-
output(info, fmt, title="Request Detail")
|
|
362
|
+
output(_record_to_detail_display(record), fmt, title="Request Detail")
|
|
230
363
|
return
|
|
231
364
|
|
|
232
365
|
# Server-side CSV export — write raw bytes to stdout and return early
|
|
@@ -266,12 +399,15 @@ def logs(
|
|
|
266
399
|
start_timestamp=start_ts,
|
|
267
400
|
end_timestamp=end_ts,
|
|
268
401
|
group=group,
|
|
402
|
+
request_id=None,
|
|
269
403
|
)
|
|
270
404
|
|
|
271
405
|
data = extract_items(resp)
|
|
272
406
|
|
|
273
|
-
|
|
274
|
-
|
|
407
|
+
# Machine formats emit the full canonical record per entry (typed values,
|
|
408
|
+
# incl. cost_usd); table/markdown/csv keep the compact summary rows.
|
|
409
|
+
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
|
|
410
|
+
output([_build_log_record(log) for log in data], fmt)
|
|
275
411
|
return
|
|
276
412
|
|
|
277
413
|
if not data:
|
cometapi_cli/constants.py
CHANGED
|
@@ -24,6 +24,13 @@ def format_ts(ts: int, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
|
24
24
|
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime(fmt)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def format_iso(ts: int) -> str | None:
|
|
28
|
+
"""Format a Unix timestamp as an ISO 8601 UTC string, or None when absent."""
|
|
29
|
+
if not ts:
|
|
30
|
+
return None
|
|
31
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
def parse_date(value: str, label: str) -> int:
|
|
28
35
|
"""Parse a date string into a Unix timestamp (UTC).
|
|
29
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cometapi-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: CometAPI CLI — official command-line interface for the CometAPI AI gateway
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/cometapi-cli/
|
|
6
6
|
Project-URL: Documentation, https://apidoc.cometapi.com/libraries/cli/overview
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Internet
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
24
25
|
Requires-Dist: openai>=1.0.0
|
|
25
26
|
Requires-Dist: prompt-toolkit>=3.0
|
|
26
27
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -144,6 +145,21 @@ cometapi run -h
|
|
|
144
145
|
| `repl` | Start an interactive command shell | Depends on command used |
|
|
145
146
|
| `config` | Show, set, unset, or locate local configuration | None |
|
|
146
147
|
|
|
148
|
+
## Logs
|
|
149
|
+
|
|
150
|
+
Use `cometapi logs` to inspect recent usage, export CSV, or look up one request by the
|
|
151
|
+
`X-Cometapi-Request-Id` response header.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cometapi logs --limit 20
|
|
155
|
+
cometapi logs --type consume --start 2026-06-01 --json
|
|
156
|
+
cometapi logs --request-id 20260617165550885561292gJBlzjtp
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`logs --request-id` first asks the backend for that exact request ID. If the installed
|
|
160
|
+
backend does not support direct request-ID filtering yet, the CLI falls back to scanning
|
|
161
|
+
a bounded number of log pages. Use `--request-id-max-pages` to widen that fallback scan.
|
|
162
|
+
|
|
147
163
|
## Models
|
|
148
164
|
|
|
149
165
|
`cometapi models` uses the public model catalog by default and displays richer metadata than `/v1/models`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
cometapi_cli/__init__.py,sha256=
|
|
1
|
+
cometapi_cli/__init__.py,sha256=I37ML-zws7ZAjDJSAw87LxYgHMsTcaVOz67of_juqiI,92
|
|
2
2
|
cometapi_cli/app.py,sha256=d2IZJabWmdZtPtFNOB4YiuSJ1GYvO0hgIOkjm4gQjxA,2948
|
|
3
3
|
cometapi_cli/catalog.py,sha256=Pc0XGoskMEKIzfbU2H_0Yi8zecLzKl1ss8fd2mV7778,4929
|
|
4
|
-
cometapi_cli/client.py,sha256=
|
|
4
|
+
cometapi_cli/client.py,sha256=QBtQ1cvmwvwjpIYpzupC3BMNqxREhMoRnzgCZOeJnEE,9998
|
|
5
5
|
cometapi_cli/config.py,sha256=oJXQidKCOsKNYPnE8OfLLoOfsv0MSZEDICB6VShJRSA,3307
|
|
6
6
|
cometapi_cli/console.py,sha256=HFSU1gL9SDmwjBvgVgDraoaU50oTwCRpsAwYXYlVP9o,163
|
|
7
|
-
cometapi_cli/constants.py,sha256=
|
|
7
|
+
cometapi_cli/constants.py,sha256=UPfU36fnRTl4JdcyjySQ0UEVign3i_UTNH9mQ-Gb4ls,1826
|
|
8
8
|
cometapi_cli/errors.py,sha256=MQ3VXefVhdIFa4uUmoLf_O5d1EFuu9PfGYrBGuq-mSA,3703
|
|
9
9
|
cometapi_cli/formatters.py,sha256=PvIGp-O9UtSFuQWKJGp3OEeDnd9L6vbHL91V5VYrqLo,4559
|
|
10
10
|
cometapi_cli/main.py,sha256=4NmO8MAAc_H30hahT6VNrdGdlaFlQ99AixKcRV5iDXE,168
|
|
@@ -16,7 +16,7 @@ cometapi_cli/commands/chat.py,sha256=xQgsjFQ33kVRfSnU5t3fbahjZE8nmVSa_w8qUTPeacE
|
|
|
16
16
|
cometapi_cli/commands/chat_repl.py,sha256=b9lkYnbbaOb0AlSpRcq3wWHmnhtTXvaqeJiUImsMEBY,8176
|
|
17
17
|
cometapi_cli/commands/config_cmd.py,sha256=OqR0TuAq6gQXw8nJIt5AAv_Fk4zdfbg-KhNLuUsrEsU,8619
|
|
18
18
|
cometapi_cli/commands/doctor.py,sha256=s9XzRIkF9FIMAWSc7b3c6v3g8Px9kb0k6WDucOrdXY8,5360
|
|
19
|
-
cometapi_cli/commands/logs.py,sha256=
|
|
19
|
+
cometapi_cli/commands/logs.py,sha256=__6Ft5TpBwVtRPTkFbmCjnriGfb1CBNGgGH_CWFmOeY,16082
|
|
20
20
|
cometapi_cli/commands/model.py,sha256=YyMRt6enCAZfxW_dVcNvdNgYSl-J4iOqPlSsv14nfJs,2654
|
|
21
21
|
cometapi_cli/commands/models.py,sha256=1TQC8LJ6JASErkLG5XtaZyRrq4vr4sVWH7eo426FeNs,8824
|
|
22
22
|
cometapi_cli/commands/repl.py,sha256=b5z1jmEXOsCrb6fwEUyv-IKot929NIPQQAbA1uas1D4,4283
|
|
@@ -24,8 +24,8 @@ cometapi_cli/commands/run.py,sha256=JQQ1DFSyJoD4pByLmhMht1-XhDDZpV94F0hshXL7hKY,
|
|
|
24
24
|
cometapi_cli/commands/stats.py,sha256=bEVywsom7bm8AGKGWlgAWTFjOp4NTJxDk6YEEvECM6U,1411
|
|
25
25
|
cometapi_cli/commands/tasks.py,sha256=NBKOKrDow52sVjoM0ezwS9IhVRltlqYLmpamoEUbAWA,4718
|
|
26
26
|
cometapi_cli/commands/tokens.py,sha256=U0AI8T690NJxAKFBwrWrsr3izXIiB_PZ9avmwpdgM9A,3042
|
|
27
|
-
cometapi_cli-0.3.
|
|
28
|
-
cometapi_cli-0.3.
|
|
29
|
-
cometapi_cli-0.3.
|
|
30
|
-
cometapi_cli-0.3.
|
|
31
|
-
cometapi_cli-0.3.
|
|
27
|
+
cometapi_cli-0.3.2.dist-info/METADATA,sha256=WJ4GHqBwefcOBQJ4A5jF3vLTcqotUOUmd4pAy0dld_s,10646
|
|
28
|
+
cometapi_cli-0.3.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
29
|
+
cometapi_cli-0.3.2.dist-info/entry_points.txt,sha256=xoiE2ZVNNWXTq0JRBtzC8hJ2JkdKuaBNz_fEd8OJpLs,50
|
|
30
|
+
cometapi_cli-0.3.2.dist-info/licenses/LICENSE,sha256=-rBwHQzkmLbty07abmGvQvsRrvDeEQUkPDhNJfTcjdE,1065
|
|
31
|
+
cometapi_cli-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|