cometapi-cli 0.3.1__tar.gz → 0.3.2__tar.gz

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.
Files changed (82) hide show
  1. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/AGENTS.md +3 -0
  2. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CHANGELOG.md +14 -0
  3. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/PKG-INFO +17 -1
  4. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/README.md +15 -0
  5. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/logs.md +16 -0
  6. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/pyproject.toml +2 -1
  7. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/__init__.py +1 -1
  8. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/client.py +15 -1
  9. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/logs.py +90 -14
  10. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/conftest.py +5 -1
  11. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_logs.py +73 -3
  12. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/uv.lock +3 -1
  13. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/workflows/ci.yml +0 -0
  17. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/workflows/publish.yml +0 -0
  18. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.gitignore +0 -0
  19. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CODE_OF_CONDUCT.md +0 -0
  20. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CONTRIBUTING.md +0 -0
  21. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/LICENSE +0 -0
  22. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/SECURITY.md +0 -0
  23. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/SKILL.md +0 -0
  24. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/README.md +0 -0
  25. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/authentication.md +0 -0
  26. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/account.md +0 -0
  27. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/balance.md +0 -0
  28. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/chat.md +0 -0
  29. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/config.md +0 -0
  30. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/doctor.md +0 -0
  31. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/init.md +0 -0
  32. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/model.md +0 -0
  33. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/models.md +0 -0
  34. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/repl.md +0 -0
  35. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/run.md +0 -0
  36. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/stats.md +0 -0
  37. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/tasks.md +0 -0
  38. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/tokens.md +0 -0
  39. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/configuration.md +0 -0
  40. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/errors.md +0 -0
  41. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/installation.md +0 -0
  42. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/output-formats.md +0 -0
  43. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/skills/live-test/SKILL.md +0 -0
  44. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/app.py +0 -0
  45. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/catalog.py +0 -0
  46. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/__init__.py +0 -0
  47. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/account.py +0 -0
  48. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/balance.py +0 -0
  49. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/chat.py +0 -0
  50. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/chat_repl.py +0 -0
  51. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/config_cmd.py +0 -0
  52. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/doctor.py +0 -0
  53. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/model.py +0 -0
  54. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/models.py +0 -0
  55. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/repl.py +0 -0
  56. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/run.py +0 -0
  57. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/stats.py +0 -0
  58. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/tasks.py +0 -0
  59. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/tokens.py +0 -0
  60. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/config.py +0 -0
  61. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/console.py +0 -0
  62. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/constants.py +0 -0
  63. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/errors.py +0 -0
  64. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/formatters.py +0 -0
  65. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/main.py +0 -0
  66. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/urls.py +0 -0
  67. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/__init__.py +0 -0
  68. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_account.py +0 -0
  69. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_balance.py +0 -0
  70. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_catalog.py +0 -0
  71. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_chat.py +0 -0
  72. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_config.py +0 -0
  73. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_doctor.py +0 -0
  74. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_errors.py +0 -0
  75. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_formatters.py +0 -0
  76. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_help.py +0 -0
  77. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_model_info.py +0 -0
  78. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_models.py +0 -0
  79. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_run.py +0 -0
  80. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_stats.py +0 -0
  81. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_tasks.py +0 -0
  82. {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_tokens.py +0 -0
@@ -353,6 +353,8 @@ These options apply to **every** command via the root Typer callback.
353
353
  | `--start` | — | `str` | `None` | Start date (`YYYY-MM-DD`, ISO 8601, or Unix timestamp) |
354
354
  | `--end` | — | `str` | `None` | End date (`YYYY-MM-DD`, ISO 8601, or Unix timestamp) |
355
355
  | `--group` | `-g` | `str` | `None` | Filter by API key group |
356
+ | `--request-id` | — | `str` | `None` | Look up one request by `X-Cometapi-Request-Id` |
357
+ | `--request-id-max-pages` | — | `int` | `10` | Fallback pages to scan when direct request-ID lookup is unavailable |
356
358
  | `--page` | `-p` | `int` | `1` | Page number |
357
359
  | `--limit` | `-l` | `int` | `20` | Results per page |
358
360
  | `--export` | — | flag | `false` | Export logs as server-side CSV to stdout |
@@ -375,6 +377,7 @@ Invalid `--type` values produce an error message with valid options and exit cod
375
377
 
376
378
  **Behavior:**
377
379
  - Without `--search` or `--export`: calls `client.list_logs(page=, page_size=, log_type=, model_name=, token_name=, start_timestamp=, end_timestamp=, group=)` — paginated, `data.items`.
380
+ - With `--request-id`: first calls `client.list_logs(..., request_id=)` and verifies an exact match. If the backend ignores `request_id`, falls back to scanning up to `--request-id-max-pages` pages; timestamp-shaped CometAPI request IDs are narrowed to the inferred local request day unless `--start`/`--end` were provided.
378
381
  - With `--search`: calls `client.search_logs(keyword=)` — flat `data` list, ignores other filter flags.
379
382
  - With `--export`: calls `client.export_logs(...)` — writes server-side CSV bytes to stdout. Honors `--model`, `--token-name`, `--type`, `--start`, `--end`, `--group`. Pipe-friendly (no Rich formatting).
380
383
 
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2] — 2026-06-17
9
+
10
+ ### Fixed
11
+
12
+ - `logs --request-id` now tries a direct request-ID lookup before falling back to paginated scanning. This avoids treating a precise lookup as an unbounded recent-log scan when the backend supports `request_id` filtering.
13
+ - `logs --request-id` fallback now narrows timestamp-shaped CometAPI request IDs to their inferred local request day, reducing slow scans on large log histories.
14
+ - Account-management requests now use bounded timeouts, so slow log pages fail promptly instead of appearing to hang for several minutes.
15
+ - Missing request-ID messages now explain the direct lookup and bounded fallback scan instead of implying only that the entry is older than the search window.
16
+ - Packaged installs now declare the CLI's direct `click` dependency instead of relying on Typer's transitive dependency metadata.
17
+
18
+ ### Added
19
+
20
+ - `logs --request-id-max-pages` controls how many fallback pages are scanned when direct request-ID lookup is unavailable.
21
+
8
22
  ## [0.3.1] — 2026-05-29
9
23
 
10
24
  ### Fixed
@@ -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`.
@@ -109,6 +109,21 @@ cometapi run -h
109
109
  | `repl` | Start an interactive command shell | Depends on command used |
110
110
  | `config` | Show, set, unset, or locate local configuration | None |
111
111
 
112
+ ## Logs
113
+
114
+ Use `cometapi logs` to inspect recent usage, export CSV, or look up one request by the
115
+ `X-Cometapi-Request-Id` response header.
116
+
117
+ ```bash
118
+ cometapi logs --limit 20
119
+ cometapi logs --type consume --start 2026-06-01 --json
120
+ cometapi logs --request-id 20260617165550885561292gJBlzjtp
121
+ ```
122
+
123
+ `logs --request-id` first asks the backend for that exact request ID. If the installed
124
+ backend does not support direct request-ID filtering yet, the CLI falls back to scanning
125
+ a bounded number of log pages. Use `--request-id-max-pages` to widen that fallback scan.
126
+
112
127
  ## Models
113
128
 
114
129
  `cometapi models` uses the public model catalog by default and displays richer metadata than `/v1/models`.
@@ -18,6 +18,7 @@ cometapi logs [OPTIONS]
18
18
  | `--end` | — | string | — | End date (see [Date Formats](#date-formats)) |
19
19
  | `--group` | `-g` | string | — | Filter by API key group |
20
20
  | `--request-id` | — | string | — | Look up cost by request ID (from `X-Cometapi-Request-Id` header) |
21
+ | `--request-id-max-pages` | — | int | `10` | Fallback pages to scan when direct request-ID lookup is unavailable |
21
22
  | `--detail` | — | flag | `false` | Show extended columns (request ID, pricing ratios) |
22
23
  | `--page` | `-p` | int | `1` | Page number |
23
24
  | `--limit` | `-l` | int | `20` | Results per page |
@@ -163,6 +164,21 @@ cometapi logs --request-id req-abc-123
163
164
  cometapi logs --request-id req-abc-123 --json
164
165
  ```
165
166
 
167
+ Lookup behavior:
168
+
169
+ 1. The CLI first asks the backend for `request_id=req-abc-123` and accepts only an exact
170
+ `request_id` match.
171
+ 2. If the backend does not return an exact match, the CLI falls back to scanning recent
172
+ log pages. Timestamp-shaped CometAPI request IDs are narrowed to the inferred local
173
+ request day unless you provide `--start` or `--end`.
174
+ 3. The fallback scan is bounded by `--request-id-max-pages` (default: `10`) so a slow log
175
+ endpoint does not appear to hang indefinitely.
176
+
177
+ ```bash
178
+ # Widen fallback scanning when direct lookup is unavailable
179
+ cometapi logs --request-id req-abc-123 --request-id-max-pages 25
180
+ ```
181
+
166
182
  Output shows a request detail card:
167
183
 
168
184
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cometapi-cli"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "CometAPI CLI — official command-line interface for the CometAPI AI gateway"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Typing :: Typed",
25
25
  ]
26
26
  dependencies = [
27
+ "click>=8.0",
27
28
  "openai>=1.0.0",
28
29
  "typer>=0.12",
29
30
  "rich>=13.0",
@@ -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"
@@ -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)
@@ -176,7 +176,11 @@ def mock_client():
176
176
  "is_stream": True,
177
177
  "request_id": "req-abc-001",
178
178
  "response_id": "chatcmpl-xyz-001",
179
- "other": '{"model_ratio":0.625,"completion_ratio":8,"group_ratio":0.8,"model_price":-1,"frt":120,"total_ms":1200,"request_path":"/v1/chat/completions"}',
179
+ "other": (
180
+ '{"model_ratio":0.625,"completion_ratio":8,"group_ratio":0.8,'
181
+ '"model_price":-1,"frt":120,"total_ms":1200,'
182
+ '"request_path":"/v1/chat/completions"}'
183
+ ),
180
184
  },
181
185
  {
182
186
  "id": 101,
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from datetime import datetime, timezone
6
7
 
7
8
 
8
9
  class TestLogsTable:
@@ -28,6 +29,7 @@ class TestLogsTable:
28
29
  page=1, page_size=20, log_type=None,
29
30
  model_name="gpt-5.4", token_name=None,
30
31
  start_timestamp=None, end_timestamp=None, group=None,
32
+ request_id=None,
31
33
  )
32
34
 
33
35
  def test_logs_filter_type(self, cli_runner, patched_client):
@@ -37,6 +39,7 @@ class TestLogsTable:
37
39
  page=1, page_size=20, log_type=2,
38
40
  model_name=None, token_name=None,
39
41
  start_timestamp=None, end_timestamp=None, group=None,
42
+ request_id=None,
40
43
  )
41
44
 
42
45
  def test_logs_filter_token_name(self, cli_runner, patched_client):
@@ -46,6 +49,7 @@ class TestLogsTable:
46
49
  page=1, page_size=20, log_type=None,
47
50
  model_name=None, token_name="prod",
48
51
  start_timestamp=None, end_timestamp=None, group=None,
52
+ request_id=None,
49
53
  )
50
54
 
51
55
  def test_logs_invalid_type(self, cli_runner, patched_client):
@@ -59,6 +63,7 @@ class TestLogsTable:
59
63
  page=3, page_size=50, log_type=None,
60
64
  model_name=None, token_name=None,
61
65
  start_timestamp=None, end_timestamp=None, group=None,
66
+ request_id=None,
62
67
  )
63
68
 
64
69
  def test_logs_filter_start_date(self, cli_runner, patched_client):
@@ -68,6 +73,7 @@ class TestLogsTable:
68
73
  page=1, page_size=20, log_type=None,
69
74
  model_name=None, token_name=None,
70
75
  start_timestamp=1705276800, end_timestamp=None, group=None,
76
+ request_id=None,
71
77
  )
72
78
 
73
79
  def test_logs_filter_end_date(self, cli_runner, patched_client):
@@ -77,6 +83,7 @@ class TestLogsTable:
77
83
  page=1, page_size=20, log_type=None,
78
84
  model_name=None, token_name=None,
79
85
  start_timestamp=None, end_timestamp=1706659200, group=None,
86
+ request_id=None,
80
87
  )
81
88
 
82
89
  def test_logs_filter_date_range(self, cli_runner, patched_client):
@@ -86,6 +93,7 @@ class TestLogsTable:
86
93
  page=1, page_size=20, log_type=None,
87
94
  model_name=None, token_name=None,
88
95
  start_timestamp=1705276800, end_timestamp=1706659200, group=None,
96
+ request_id=None,
89
97
  )
90
98
 
91
99
  def test_logs_filter_unix_timestamp(self, cli_runner, patched_client):
@@ -95,6 +103,7 @@ class TestLogsTable:
95
103
  page=1, page_size=20, log_type=None,
96
104
  model_name=None, token_name=None,
97
105
  start_timestamp=1705276800, end_timestamp=None, group=None,
106
+ request_id=None,
98
107
  )
99
108
 
100
109
  def test_logs_invalid_date(self, cli_runner, patched_client):
@@ -108,6 +117,7 @@ class TestLogsTable:
108
117
  page=1, page_size=20, log_type=None,
109
118
  model_name=None, token_name=None,
110
119
  start_timestamp=None, end_timestamp=None, group="default",
120
+ request_id=None,
111
121
  )
112
122
 
113
123
  def test_logs_all_filters(self, cli_runner, patched_client):
@@ -121,6 +131,7 @@ class TestLogsTable:
121
131
  page=1, page_size=20, log_type=2,
122
132
  model_name="gpt-5.4", token_name=None,
123
133
  start_timestamp=1705276800, end_timestamp=1706659200, group="default",
134
+ request_id=None,
124
135
  )
125
136
 
126
137
  def test_logs_export(self, cli_runner, patched_client):
@@ -179,7 +190,12 @@ class TestLogsRequestId:
179
190
  # Should show the full log: model, tokens, cost, pricing ratios
180
191
  assert "gpt-5.4" in result.output
181
192
  assert "$0.0003" in result.output # 150 / 500_000
182
- patched_client.list_logs.assert_called()
193
+ patched_client.list_logs.assert_called_once_with(
194
+ page=1, page_size=100, log_type=None,
195
+ model_name=None, token_name=None,
196
+ start_timestamp=None, end_timestamp=None, group=None,
197
+ request_id="req-abc-001",
198
+ )
183
199
 
184
200
  def test_logs_request_id_json(self, cli_runner, patched_client):
185
201
  result = cli_runner("logs", "--request-id", "req-abc-001", "--json")
@@ -203,6 +219,7 @@ class TestLogsRequestId:
203
219
  def test_logs_request_id_not_found(self, cli_runner, patched_client):
204
220
  result = cli_runner("logs", "--request-id", "nonexistent-id")
205
221
  assert result.exit_code != 0
222
+ assert "Tried direct request-id lookup first" in result.output
206
223
 
207
224
  def test_logs_request_id_with_export_conflict(self, cli_runner, patched_client):
208
225
  result = cli_runner("logs", "--request-id", "req-abc-001", "--export")
@@ -211,7 +228,61 @@ class TestLogsRequestId:
211
228
  def test_logs_request_id_with_type_filter(self, cli_runner, patched_client):
212
229
  result = cli_runner("logs", "--request-id", "req-abc-001", "--type", "consume")
213
230
  assert result.exit_code == 0
214
- patched_client.list_logs.assert_called()
231
+ patched_client.list_logs.assert_called_once_with(
232
+ page=1, page_size=100, log_type=2,
233
+ model_name=None, token_name=None,
234
+ start_timestamp=None, end_timestamp=None, group=None,
235
+ request_id="req-abc-001",
236
+ )
237
+
238
+ def test_logs_request_id_direct_lookup_ignores_non_matching_backend_item(
239
+ self, cli_runner, patched_client
240
+ ):
241
+ direct_ignored = {
242
+ "data": {
243
+ "items": [
244
+ {
245
+ "request_id": "different-id",
246
+ "model_name": "gpt-5.4",
247
+ "created_at": 1700100000,
248
+ "type": 2,
249
+ }
250
+ ]
251
+ }
252
+ }
253
+ patched_client.list_logs.side_effect = [direct_ignored, patched_client.list_logs.return_value]
254
+
255
+ result = cli_runner("logs", "--request-id", "req-abc-001")
256
+
257
+ assert result.exit_code == 0
258
+ assert patched_client.list_logs.call_count == 2
259
+ first_call = patched_client.list_logs.call_args_list[0]
260
+ fallback_call = patched_client.list_logs.call_args_list[1]
261
+ assert first_call.kwargs["request_id"] == "req-abc-001"
262
+ assert "request_id" not in fallback_call.kwargs
263
+
264
+ def test_logs_request_id_fallback_uses_inferred_date_window(self, cli_runner, patched_client):
265
+ patched_client.list_logs.return_value = {"data": {"items": []}}
266
+
267
+ result = cli_runner("logs", "--request-id", "20260617165550885561292gJBlzjtp")
268
+
269
+ assert result.exit_code != 0
270
+ fallback_call = patched_client.list_logs.call_args_list[1]
271
+ expected_start = int(datetime(2026, 6, 16, 16, 0, tzinfo=timezone.utc).timestamp())
272
+ expected_end = int(datetime(2026, 6, 17, 15, 59, 59, tzinfo=timezone.utc).timestamp())
273
+ assert fallback_call.kwargs["start_timestamp"] == expected_start
274
+ assert fallback_call.kwargs["end_timestamp"] == expected_end
275
+
276
+ def test_logs_request_id_max_pages_controls_fallback_scan(self, cli_runner, patched_client):
277
+ patched_client.list_logs.return_value = {"data": {"items": [{"request_id": "other"}]}}
278
+
279
+ result = cli_runner(
280
+ "logs", "--request-id", "not-a-timestamp-id", "--request-id-max-pages", "2"
281
+ )
282
+
283
+ assert result.exit_code != 0
284
+ # 1 direct request plus 2 fallback pages.
285
+ assert patched_client.list_logs.call_count == 3
215
286
 
216
287
 
217
288
  class TestLogsDetail:
@@ -230,4 +301,3 @@ class TestLogsDetail:
230
301
  assert result.exit_code == 0
231
302
  data = json.loads(result.output)
232
303
  assert len(data) == 2
233
-
@@ -66,9 +66,10 @@ wheels = [
66
66
 
67
67
  [[package]]
68
68
  name = "cometapi-cli"
69
- version = "0.3.1"
69
+ version = "0.3.2"
70
70
  source = { editable = "." }
71
71
  dependencies = [
72
+ { name = "click" },
72
73
  { name = "openai" },
73
74
  { name = "prompt-toolkit" },
74
75
  { name = "pyyaml" },
@@ -86,6 +87,7 @@ dev = [
86
87
 
87
88
  [package.metadata]
88
89
  requires-dist = [
90
+ { name = "click", specifier = ">=8.0" },
89
91
  { name = "openai", specifier = ">=1.0.0" },
90
92
  { name = "prompt-toolkit", specifier = ">=3.0" },
91
93
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
File without changes
File without changes
File without changes
File without changes