cometapi-cli 0.3.5__tar.gz → 0.3.6__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.5 → cometapi_cli-0.3.6}/AGENTS.md +1 -2
  2. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CHANGELOG.md +8 -0
  3. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/PKG-INFO +4 -4
  4. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/README.md +3 -3
  5. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/logs.md +24 -12
  6. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/pyproject.toml +1 -1
  7. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/__init__.py +1 -1
  8. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/client.py +27 -3
  9. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/logs.py +10 -86
  10. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/conftest.py +31 -0
  11. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_logs.py +10 -47
  12. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/uv.lock +1 -1
  13. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/workflows/ci.yml +0 -0
  17. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/workflows/publish.yml +0 -0
  18. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.gitignore +0 -0
  19. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CODE_OF_CONDUCT.md +0 -0
  20. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CONTRIBUTING.md +0 -0
  21. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/LICENSE +0 -0
  22. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/SECURITY.md +0 -0
  23. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/SKILL.md +0 -0
  24. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/README.md +0 -0
  25. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/authentication.md +0 -0
  26. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/account.md +0 -0
  27. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/balance.md +0 -0
  28. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/chat.md +0 -0
  29. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/config.md +0 -0
  30. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/doctor.md +0 -0
  31. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/init.md +0 -0
  32. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/model.md +0 -0
  33. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/models.md +0 -0
  34. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/repl.md +0 -0
  35. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/run.md +0 -0
  36. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/stats.md +0 -0
  37. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/tasks.md +0 -0
  38. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/tokens.md +0 -0
  39. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/configuration.md +0 -0
  40. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/errors.md +0 -0
  41. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/installation.md +0 -0
  42. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/output-formats.md +0 -0
  43. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/skills/live-test/SKILL.md +0 -0
  44. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/app.py +0 -0
  45. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/catalog.py +0 -0
  46. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/__init__.py +0 -0
  47. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/account.py +0 -0
  48. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/balance.py +0 -0
  49. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/chat.py +0 -0
  50. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/chat_repl.py +0 -0
  51. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/config_cmd.py +0 -0
  52. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/doctor.py +0 -0
  53. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/model.py +0 -0
  54. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/models.py +0 -0
  55. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/repl.py +0 -0
  56. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/run.py +0 -0
  57. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/stats.py +0 -0
  58. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/tasks.py +0 -0
  59. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/tokens.py +0 -0
  60. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/config.py +0 -0
  61. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/console.py +0 -0
  62. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/constants.py +0 -0
  63. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/errors.py +0 -0
  64. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/formatters.py +0 -0
  65. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/main.py +0 -0
  66. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/urls.py +0 -0
  67. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/__init__.py +0 -0
  68. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_account.py +0 -0
  69. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_balance.py +0 -0
  70. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_catalog.py +0 -0
  71. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_chat.py +0 -0
  72. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_config.py +0 -0
  73. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_doctor.py +0 -0
  74. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_errors.py +0 -0
  75. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_formatters.py +0 -0
  76. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_help.py +0 -0
  77. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_model_info.py +0 -0
  78. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_models.py +0 -0
  79. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_run.py +0 -0
  80. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_stats.py +0 -0
  81. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_tasks.py +0 -0
  82. {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_tokens.py +0 -0
@@ -354,7 +354,6 @@ These options apply to **every** command via the root Typer callback.
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
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 |
358
357
  | `--page` | `-p` | `int` | `1` | Page number |
359
358
  | `--limit` | `-l` | `int` | `20` | Results per page |
360
359
  | `--export` | — | flag | `false` | Export logs as server-side CSV to stdout |
@@ -377,7 +376,7 @@ Invalid `--type` values produce an error message with valid options and exit cod
377
376
 
378
377
  **Behavior:**
379
378
  - 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.
379
+ - With `--request-id`: calls `client.lookup_log(request_id=, ...)`, which uses `/api/log` and verifies an exact match. It does not scan `/api/log/self` pages because that backend endpoint does not currently apply `request_id` filtering.
381
380
  - With `--search`: calls `client.search_logs(keyword=)` — flat `data` list, ignores other filter flags.
382
381
  - 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).
383
382
 
@@ -5,6 +5,14 @@ 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.6] — 2026-06-17
9
+
10
+ ### Fixed
11
+
12
+ - `logs --request-id` now uses the indexed operator log endpoint (`/api/log`) and
13
+ performs exactly one exact-match lookup. The CLI no longer falls back to scanning
14
+ `/api/log/self` pages because that backend endpoint currently ignores `request_id`.
15
+
8
16
  ## [0.3.5] — 2026-06-17
9
17
 
10
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cometapi-cli
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -156,9 +156,9 @@ cometapi logs --type consume --start 2026-06-01 --json
156
156
  cometapi logs --request-id 20260617165550885561292gJBlzjtp
157
157
  ```
158
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.
159
+ `logs --request-id` performs one indexed lookup against the operator log endpoint and
160
+ accepts only an exact `request_id` match. The CLI does not scan fallback log pages,
161
+ because the self-log endpoint does not currently apply `request_id` filtering.
162
162
 
163
163
  ## Models
164
164
 
@@ -120,9 +120,9 @@ cometapi logs --type consume --start 2026-06-01 --json
120
120
  cometapi logs --request-id 20260617165550885561292gJBlzjtp
121
121
  ```
122
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.
123
+ `logs --request-id` performs one indexed lookup against the operator log endpoint and
124
+ accepts only an exact `request_id` match. The CLI does not scan fallback log pages,
125
+ because the self-log endpoint does not currently apply `request_id` filtering.
126
126
 
127
127
  ## Models
128
128
 
@@ -18,7 +18,6 @@ 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 |
22
21
  | `--detail` | — | flag | `false` | Show extended columns (request ID, pricing ratios) |
23
22
  | `--page` | `-p` | int | `1` | Page number |
24
23
  | `--limit` | `-l` | int | `20` | Results per page |
@@ -166,18 +165,11 @@ cometapi logs --request-id req-abc-123 --json
166
165
 
167
166
  Lookup behavior:
168
167
 
169
- 1. The CLI first asks the backend for `request_id=req-abc-123` and accepts only an exact
168
+ 1. The CLI asks the operator log endpoint for `request_id=req-abc-123` and accepts only an exact
170
169
  `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
- ```
170
+ 2. The lookup uses `/api/log`, which requires an operations/admin access token.
171
+ 3. The CLI does not scan fallback pages. The self-log endpoint does not currently apply
172
+ `request_id` filtering, so scanning it would waste backend resources.
181
173
 
182
174
  Output shows a request detail card:
183
175
 
@@ -329,6 +321,26 @@ GET /api/log/self
329
321
  }
330
322
  ```
331
323
 
324
+ ### Exact Request ID Lookup
325
+
326
+ ```
327
+ GET /api/log
328
+ ```
329
+
330
+ The CLI uses this operations/admin endpoint for `--request-id` because it applies
331
+ `request_id` as an indexed backend filter. The personal log list endpoint
332
+ (`/api/log/self`) does not currently apply `request_id` filtering.
333
+
334
+ | Parameter | Type | Description |
335
+ |-----------|------|-------------|
336
+ | `request_id` | string | Exact request ID from `X-Cometapi-Request-Id` |
337
+ | `type` | int | Optional log type code |
338
+ | `model_name` | string | Optional model filter |
339
+ | `token_name` | string | Optional token/API key name filter |
340
+ | `group` | string | Optional API key group filter |
341
+ | `start_timestamp` | int | Optional start time |
342
+ | `end_timestamp` | int | Optional end time |
343
+
332
344
  ### Log Response Fields
333
345
 
334
346
  | Field | Type | Description |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cometapi-cli"
3
- version = "0.3.5"
3
+ version = "0.3.6"
4
4
  description = "CometAPI CLI — official command-line interface for the CometAPI AI gateway"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """CometAPI CLI — professional terminal interface for CometAPI."""
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.3.6"
@@ -161,7 +161,6 @@ class CometClient(openai.OpenAI):
161
161
  start_timestamp: int | None = None,
162
162
  end_timestamp: int | None = None,
163
163
  group: str | None = None,
164
- request_id: str | None = None,
165
164
  ) -> dict:
166
165
  """List the user's usage logs (requires access token)."""
167
166
  params: dict[str, Any] = {"p": page, "page_size": page_size}
@@ -177,10 +176,35 @@ class CometClient(openai.OpenAI):
177
176
  params["end_timestamp"] = end_timestamp
178
177
  if group:
179
178
  params["group"] = group
180
- if request_id:
181
- params["request_id"] = request_id
182
179
  return self._account_request("GET", "/api/log/self", params=params)
183
180
 
181
+ def lookup_log(
182
+ self,
183
+ *,
184
+ request_id: str,
185
+ log_type: int | None = None,
186
+ model_name: str | None = None,
187
+ token_name: str | None = None,
188
+ start_timestamp: int | None = None,
189
+ end_timestamp: int | None = None,
190
+ group: str | None = None,
191
+ ) -> dict:
192
+ """Look up one usage log by request ID using the indexed operator endpoint."""
193
+ params: dict[str, Any] = {"p": 1, "page_size": 1, "request_id": request_id}
194
+ if log_type is not None:
195
+ params["type"] = log_type
196
+ if model_name:
197
+ params["model_name"] = model_name
198
+ if token_name:
199
+ params["token_name"] = token_name
200
+ if start_timestamp is not None:
201
+ params["start_timestamp"] = start_timestamp
202
+ if end_timestamp is not None:
203
+ params["end_timestamp"] = end_timestamp
204
+ if group:
205
+ params["group"] = group
206
+ return self._account_request("GET", "/api/log/", params=params)
207
+
184
208
  def search_logs(self, keyword: str) -> dict:
185
209
  """Search usage logs by keyword (requires access token)."""
186
210
  return self._account_request("GET", "/api/log/self/search", params={"keyword": keyword})
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import json as _json
6
6
  import sys
7
- from datetime import datetime, time, timedelta, timezone
8
7
  from typing import Annotated
9
8
 
10
9
  import typer
@@ -32,9 +31,6 @@ LOG_TYPE_MAP = {
32
31
  }
33
32
 
34
33
  _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))
38
34
 
39
35
 
40
36
  def _parse_other(other_raw: str | None) -> dict:
@@ -48,21 +44,6 @@ def _parse_other(other_raw: str | None) -> dict:
48
44
  return {}
49
45
 
50
46
 
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
47
  def _exact_request_id_match(items: list, request_id: str) -> dict | None:
67
48
  for item in items:
68
49
  if isinstance(item, dict) and item.get("request_id") == request_id:
@@ -80,59 +61,18 @@ def _lookup_log_by_id(
80
61
  start_timestamp: int | None = None,
81
62
  end_timestamp: int | None = None,
82
63
  group: str | None = None,
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,
64
+ ) -> dict | None:
65
+ """Look up a log entry by request ID without falling back to page scans."""
66
+ resp = client.lookup_log( # type: ignore[union-attr]
67
+ request_id=request_id,
98
68
  log_type=log_type,
99
69
  model_name=model_name,
100
70
  token_name=token_name,
101
71
  start_timestamp=start_timestamp,
102
72
  end_timestamp=end_timestamp,
103
73
  group=group,
104
- request_id=request_id,
105
74
  )
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
-
118
- for pg in range(1, max_pages + 1):
119
- resp = client.list_logs( # type: ignore[union-attr]
120
- page=pg,
121
- page_size=REQUEST_ID_PAGE_SIZE,
122
- log_type=log_type,
123
- model_name=model_name,
124
- token_name=token_name,
125
- start_timestamp=scan_start,
126
- end_timestamp=scan_end,
127
- group=group,
128
- )
129
- meta["scanned_pages"] = pg
130
- items = extract_items(resp)
131
- if not items:
132
- break
133
- if match := _exact_request_id_match(items, request_id):
134
- return match, meta
135
- return None, meta
75
+ return _exact_request_id_match(extract_items(resp), request_id)
136
76
 
137
77
 
138
78
  def _build_log_record(log: dict) -> dict:
@@ -284,14 +224,6 @@ def logs(
284
224
  str | None,
285
225
  typer.Option("--request-id", help="Look up cost by request ID (X-Cometapi-Request-Id header)."),
286
226
  ] = 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,
295
227
  detail: Annotated[
296
228
  bool,
297
229
  typer.Option("--detail", help="Show extended columns (request ID, pricing ratios)."),
@@ -325,7 +257,7 @@ def logs(
325
257
  if log_type:
326
258
  type_int = LOG_TYPE_MAP.get(log_type.lower())
327
259
 
328
- log_entry, lookup_meta = _lookup_log_by_id(
260
+ log_entry = _lookup_log_by_id(
329
261
  client,
330
262
  request_id=request_id,
331
263
  log_type=type_int,
@@ -334,22 +266,15 @@ def logs(
334
266
  start_timestamp=start_ts,
335
267
  end_timestamp=end_ts,
336
268
  group=group,
337
- max_pages=request_id_max_pages,
338
269
  )
339
270
 
340
271
  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)."
348
272
  err_console.print(
349
273
  f"[red]No log found for request_id=[/]{request_id}\n"
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.[/]"
274
+ "[dim]Tried one indexed lookup against /api/log with request_id. "
275
+ "No fallback page scan was performed because /api/log/self does not "
276
+ "currently apply request_id filtering. Use an operations/admin access token "
277
+ "or update the backend self-log endpoint to support request_id filtering.[/]"
353
278
  )
354
279
  raise typer.Exit(code=1)
355
280
 
@@ -399,7 +324,6 @@ def logs(
399
324
  start_timestamp=start_ts,
400
325
  end_timestamp=end_ts,
401
326
  group=group,
402
- request_id=None,
403
327
  )
404
328
 
405
329
  data = extract_items(resp)
@@ -200,6 +200,37 @@ def mock_client():
200
200
  ],
201
201
  }
202
202
  }
203
+ client.lookup_log.return_value = {
204
+ "data": {
205
+ "page": 1,
206
+ "page_size": 1,
207
+ "total": 1,
208
+ "items": [
209
+ {
210
+ "id": 100,
211
+ "created_at": 1700100000,
212
+ "type": 2,
213
+ "model_name": "gpt-5.4",
214
+ "token_name": "Production Key",
215
+ "username": "google_9550",
216
+ "user_id": 9550,
217
+ "ip": "203.0.113.7",
218
+ "quota": 150,
219
+ "prompt_tokens": 120,
220
+ "completion_tokens": 45,
221
+ "use_time": 1,
222
+ "is_stream": True,
223
+ "request_id": "req-abc-001",
224
+ "response_id": "chatcmpl-xyz-001",
225
+ "other": (
226
+ '{"model_ratio":0.625,"completion_ratio":8,"group_ratio":0.8,'
227
+ '"model_price":-1,"frt":120,"total_ms":1200,'
228
+ '"request_path":"/v1/chat/completions"}'
229
+ ),
230
+ },
231
+ ],
232
+ }
233
+ }
203
234
  client.search_logs.return_value = {
204
235
  "data": [
205
236
  {
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from datetime import datetime, timezone
7
6
 
8
7
 
9
8
  class TestLogsTable:
@@ -29,7 +28,6 @@ class TestLogsTable:
29
28
  page=1, page_size=20, log_type=None,
30
29
  model_name="gpt-5.4", token_name=None,
31
30
  start_timestamp=None, end_timestamp=None, group=None,
32
- request_id=None,
33
31
  )
34
32
 
35
33
  def test_logs_filter_type(self, cli_runner, patched_client):
@@ -39,7 +37,6 @@ class TestLogsTable:
39
37
  page=1, page_size=20, log_type=2,
40
38
  model_name=None, token_name=None,
41
39
  start_timestamp=None, end_timestamp=None, group=None,
42
- request_id=None,
43
40
  )
44
41
 
45
42
  def test_logs_filter_token_name(self, cli_runner, patched_client):
@@ -49,7 +46,6 @@ class TestLogsTable:
49
46
  page=1, page_size=20, log_type=None,
50
47
  model_name=None, token_name="prod",
51
48
  start_timestamp=None, end_timestamp=None, group=None,
52
- request_id=None,
53
49
  )
54
50
 
55
51
  def test_logs_invalid_type(self, cli_runner, patched_client):
@@ -63,7 +59,6 @@ class TestLogsTable:
63
59
  page=3, page_size=50, log_type=None,
64
60
  model_name=None, token_name=None,
65
61
  start_timestamp=None, end_timestamp=None, group=None,
66
- request_id=None,
67
62
  )
68
63
 
69
64
  def test_logs_filter_start_date(self, cli_runner, patched_client):
@@ -73,7 +68,6 @@ class TestLogsTable:
73
68
  page=1, page_size=20, log_type=None,
74
69
  model_name=None, token_name=None,
75
70
  start_timestamp=1705276800, end_timestamp=None, group=None,
76
- request_id=None,
77
71
  )
78
72
 
79
73
  def test_logs_filter_end_date(self, cli_runner, patched_client):
@@ -83,7 +77,6 @@ class TestLogsTable:
83
77
  page=1, page_size=20, log_type=None,
84
78
  model_name=None, token_name=None,
85
79
  start_timestamp=None, end_timestamp=1706659200, group=None,
86
- request_id=None,
87
80
  )
88
81
 
89
82
  def test_logs_filter_date_range(self, cli_runner, patched_client):
@@ -93,7 +86,6 @@ class TestLogsTable:
93
86
  page=1, page_size=20, log_type=None,
94
87
  model_name=None, token_name=None,
95
88
  start_timestamp=1705276800, end_timestamp=1706659200, group=None,
96
- request_id=None,
97
89
  )
98
90
 
99
91
  def test_logs_filter_unix_timestamp(self, cli_runner, patched_client):
@@ -103,7 +95,6 @@ class TestLogsTable:
103
95
  page=1, page_size=20, log_type=None,
104
96
  model_name=None, token_name=None,
105
97
  start_timestamp=1705276800, end_timestamp=None, group=None,
106
- request_id=None,
107
98
  )
108
99
 
109
100
  def test_logs_invalid_date(self, cli_runner, patched_client):
@@ -117,7 +108,6 @@ class TestLogsTable:
117
108
  page=1, page_size=20, log_type=None,
118
109
  model_name=None, token_name=None,
119
110
  start_timestamp=None, end_timestamp=None, group="default",
120
- request_id=None,
121
111
  )
122
112
 
123
113
  def test_logs_all_filters(self, cli_runner, patched_client):
@@ -131,7 +121,6 @@ class TestLogsTable:
131
121
  page=1, page_size=20, log_type=2,
132
122
  model_name="gpt-5.4", token_name=None,
133
123
  start_timestamp=1705276800, end_timestamp=1706659200, group="default",
134
- request_id=None,
135
124
  )
136
125
 
137
126
  def test_logs_export(self, cli_runner, patched_client):
@@ -190,8 +179,8 @@ class TestLogsRequestId:
190
179
  # Should show the full log: model, tokens, cost, pricing ratios
191
180
  assert "gpt-5.4" in result.output
192
181
  assert "$0.0003" in result.output # 150 / 500_000
193
- patched_client.list_logs.assert_called_once_with(
194
- page=1, page_size=100, log_type=None,
182
+ patched_client.lookup_log.assert_called_once_with(
183
+ log_type=None,
195
184
  model_name=None, token_name=None,
196
185
  start_timestamp=None, end_timestamp=None, group=None,
197
186
  request_id="req-abc-001",
@@ -219,7 +208,8 @@ class TestLogsRequestId:
219
208
  def test_logs_request_id_not_found(self, cli_runner, patched_client):
220
209
  result = cli_runner("logs", "--request-id", "nonexistent-id")
221
210
  assert result.exit_code != 0
222
- assert "Tried direct request-id lookup first" in result.output
211
+ assert "No fallback" in result.output
212
+ assert "self-log" in result.output
223
213
 
224
214
  def test_logs_request_id_with_export_conflict(self, cli_runner, patched_client):
225
215
  result = cli_runner("logs", "--request-id", "req-abc-001", "--export")
@@ -228,17 +218,17 @@ class TestLogsRequestId:
228
218
  def test_logs_request_id_with_type_filter(self, cli_runner, patched_client):
229
219
  result = cli_runner("logs", "--request-id", "req-abc-001", "--type", "consume")
230
220
  assert result.exit_code == 0
231
- patched_client.list_logs.assert_called_once_with(
232
- page=1, page_size=100, log_type=2,
221
+ patched_client.lookup_log.assert_called_once_with(
222
+ log_type=2,
233
223
  model_name=None, token_name=None,
234
224
  start_timestamp=None, end_timestamp=None, group=None,
235
225
  request_id="req-abc-001",
236
226
  )
237
227
 
238
- def test_logs_request_id_direct_lookup_ignores_non_matching_backend_item(
228
+ def test_logs_request_id_lookup_ignores_non_matching_backend_item(
239
229
  self, cli_runner, patched_client
240
230
  ):
241
- direct_ignored = {
231
+ patched_client.lookup_log.return_value = {
242
232
  "data": {
243
233
  "items": [
244
234
  {
@@ -250,39 +240,12 @@ class TestLogsRequestId:
250
240
  ]
251
241
  }
252
242
  }
253
- patched_client.list_logs.side_effect = [direct_ignored, patched_client.list_logs.return_value]
254
243
 
255
244
  result = cli_runner("logs", "--request-id", "req-abc-001")
256
245
 
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
246
  assert result.exit_code != 0
284
- # 1 direct request plus 2 fallback pages.
285
- assert patched_client.list_logs.call_count == 3
247
+ patched_client.lookup_log.assert_called_once()
248
+ patched_client.list_logs.assert_not_called()
286
249
 
287
250
 
288
251
  class TestLogsDetail:
@@ -66,7 +66,7 @@ wheels = [
66
66
 
67
67
  [[package]]
68
68
  name = "cometapi-cli"
69
- version = "0.3.5"
69
+ version = "0.3.6"
70
70
  source = { editable = "." }
71
71
  dependencies = [
72
72
  { name = "click" },
File without changes
File without changes
File without changes
File without changes