cometapi-cli 0.3.1__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """CometAPI CLI — professional terminal interface for CometAPI."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.3.2"
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(self, method: str, path: str, *, params: dict | None = None) -> dict:
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
@@ -4,6 +4,7 @@ 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
@@ -31,6 +32,9 @@ LOG_TYPE_MAP = {
31
32
  }
32
33
 
33
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))
34
38
 
35
39
 
36
40
  def _parse_other(other_raw: str | None) -> dict:
@@ -44,7 +48,29 @@ def _parse_other(other_raw: str | None) -> dict:
44
48
  return {}
45
49
 
46
50
 
47
- def _find_log_by_id(
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(
48
74
  client: object,
49
75
  *,
50
76
  request_id: str,
@@ -54,27 +80,59 @@ def _find_log_by_id(
54
80
  start_timestamp: int | None = None,
55
81
  end_timestamp: int | None = None,
56
82
  group: str | None = None,
57
- max_pages: int = 10,
58
- ) -> dict | None:
59
- """Search through paginated logs to find the entry matching a request ID."""
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
+
60
118
  for pg in range(1, max_pages + 1):
61
119
  resp = client.list_logs( # type: ignore[union-attr]
62
120
  page=pg,
63
- page_size=100,
121
+ page_size=REQUEST_ID_PAGE_SIZE,
64
122
  log_type=log_type,
65
123
  model_name=model_name,
66
124
  token_name=token_name,
67
- start_timestamp=start_timestamp,
68
- end_timestamp=end_timestamp,
125
+ start_timestamp=scan_start,
126
+ end_timestamp=scan_end,
69
127
  group=group,
70
128
  )
129
+ meta["scanned_pages"] = pg
71
130
  items = extract_items(resp)
72
131
  if not items:
73
132
  break
74
- for item in items:
75
- if item.get("request_id") == request_id:
76
- return item
77
- return None
133
+ if match := _exact_request_id_match(items, request_id):
134
+ return match, meta
135
+ return None, meta
78
136
 
79
137
 
80
138
  def _build_log_record(log: dict) -> dict:
@@ -226,6 +284,14 @@ def logs(
226
284
  str | None,
227
285
  typer.Option("--request-id", help="Look up cost by request ID (X-Cometapi-Request-Id header)."),
228
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,
229
295
  detail: Annotated[
230
296
  bool,
231
297
  typer.Option("--detail", help="Show extended columns (request ID, pricing ratios)."),
@@ -259,7 +325,7 @@ def logs(
259
325
  if log_type:
260
326
  type_int = LOG_TYPE_MAP.get(log_type.lower())
261
327
 
262
- log_entry = _find_log_by_id(
328
+ log_entry, lookup_meta = _lookup_log_by_id(
263
329
  client,
264
330
  request_id=request_id,
265
331
  log_type=type_int,
@@ -268,13 +334,22 @@ def logs(
268
334
  start_timestamp=start_ts,
269
335
  end_timestamp=end_ts,
270
336
  group=group,
337
+ max_pages=request_id_max_pages,
271
338
  )
272
339
 
273
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)."
274
348
  err_console.print(
275
349
  f"[red]No log found for request_id=[/]{request_id}\n"
276
- "[dim]The entry may be older than the search window. "
277
- "Try narrowing with --start/--end or --model.[/]"
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.[/]"
278
353
  )
279
354
  raise typer.Exit(code=1)
280
355
 
@@ -324,6 +399,7 @@ def logs(
324
399
  start_timestamp=start_ts,
325
400
  end_timestamp=end_ts,
326
401
  group=group,
402
+ request_id=None,
327
403
  )
328
404
 
329
405
  data = extract_items(resp)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cometapi-cli
3
- Version: 0.3.1
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,7 +1,7 @@
1
- cometapi_cli/__init__.py,sha256=BbFx8ZMHF2P0-mvyicAQ6EU3CK9hL1k5fG8_tHoOr94,92
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=4JkMSrjWwAQV8tmlwplZQ75gzt3zHno8ASKLgMCpYkU,9658
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
7
  cometapi_cli/constants.py,sha256=UPfU36fnRTl4JdcyjySQ0UEVign3i_UTNH9mQ-Gb4ls,1826
@@ -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=1OAkfhwn6huQ1Gwd2joCqhT4pBXfCyAYAt_AzWb_VDI,13124
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.1.dist-info/METADATA,sha256=uJsV-DG8soDdbEd9IDNNyWgXUB6Ws0FEvsdTZXtHn1k,10064
28
- cometapi_cli-0.3.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
29
- cometapi_cli-0.3.1.dist-info/entry_points.txt,sha256=xoiE2ZVNNWXTq0JRBtzC8hJ2JkdKuaBNz_fEd8OJpLs,50
30
- cometapi_cli-0.3.1.dist-info/licenses/LICENSE,sha256=-rBwHQzkmLbty07abmGvQvsRrvDeEQUkPDhNJfTcjdE,1065
31
- cometapi_cli-0.3.1.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any