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.
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/AGENTS.md +1 -2
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CHANGELOG.md +8 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/PKG-INFO +4 -4
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/README.md +3 -3
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/logs.md +24 -12
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/pyproject.toml +1 -1
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/__init__.py +1 -1
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/client.py +27 -3
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/logs.py +10 -86
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/conftest.py +31 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_logs.py +10 -47
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/uv.lock +1 -1
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/workflows/ci.yml +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.github/workflows/publish.yml +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/.gitignore +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CODE_OF_CONDUCT.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/CONTRIBUTING.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/LICENSE +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/SECURITY.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/SKILL.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/README.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/authentication.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/account.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/balance.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/chat.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/config.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/doctor.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/init.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/model.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/models.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/repl.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/run.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/stats.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/tasks.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/commands/tokens.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/configuration.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/errors.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/installation.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/docs/output-formats.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/skills/live-test/SKILL.md +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/app.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/catalog.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/__init__.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/account.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/balance.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/chat.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/chat_repl.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/config_cmd.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/doctor.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/model.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/models.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/repl.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/run.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/stats.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/tasks.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/commands/tokens.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/config.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/console.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/constants.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/errors.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/formatters.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/main.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/src/cometapi_cli/urls.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/__init__.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_account.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_balance.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_catalog.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_chat.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_config.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_doctor.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_errors.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_formatters.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_help.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_model_info.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_models.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_run.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_stats.py +0 -0
- {cometapi_cli-0.3.5 → cometapi_cli-0.3.6}/tests/test_tasks.py +0 -0
- {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`:
|
|
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.
|
|
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`
|
|
160
|
-
|
|
161
|
-
|
|
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`
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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.
|
|
172
|
-
|
|
173
|
-
|
|
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 |
|
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
351
|
-
|
|
352
|
-
"
|
|
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.
|
|
194
|
-
|
|
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 "
|
|
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.
|
|
232
|
-
|
|
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
|
|
228
|
+
def test_logs_request_id_lookup_ignores_non_matching_backend_item(
|
|
239
229
|
self, cli_runner, patched_client
|
|
240
230
|
):
|
|
241
|
-
|
|
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
|
-
|
|
285
|
-
|
|
247
|
+
patched_client.lookup_log.assert_called_once()
|
|
248
|
+
patched_client.list_logs.assert_not_called()
|
|
286
249
|
|
|
287
250
|
|
|
288
251
|
class TestLogsDetail:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|