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.
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/AGENTS.md +3 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CHANGELOG.md +14 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/PKG-INFO +17 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/README.md +15 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/logs.md +16 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/pyproject.toml +2 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/__init__.py +1 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/client.py +15 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/logs.py +90 -14
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/conftest.py +5 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_logs.py +73 -3
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/uv.lock +3 -1
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/workflows/ci.yml +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.github/workflows/publish.yml +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/.gitignore +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CODE_OF_CONDUCT.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/CONTRIBUTING.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/LICENSE +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/SECURITY.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/SKILL.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/README.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/authentication.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/account.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/balance.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/chat.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/config.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/doctor.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/init.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/model.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/models.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/repl.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/run.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/stats.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/tasks.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/commands/tokens.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/configuration.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/errors.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/installation.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/docs/output-formats.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/skills/live-test/SKILL.md +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/app.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/catalog.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/__init__.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/account.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/balance.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/chat.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/chat_repl.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/config_cmd.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/doctor.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/model.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/models.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/repl.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/run.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/stats.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/tasks.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/commands/tokens.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/config.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/console.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/constants.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/errors.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/formatters.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/main.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/src/cometapi_cli/urls.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/__init__.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_account.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_balance.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_catalog.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_chat.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_config.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_doctor.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_errors.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_formatters.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_help.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_model_info.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_models.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_run.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_stats.py +0 -0
- {cometapi_cli-0.3.1 → cometapi_cli-0.3.2}/tests/test_tasks.py +0 -0
- {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.
|
|
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.
|
|
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",
|
|
@@ -9,6 +9,8 @@ import openai
|
|
|
9
9
|
|
|
10
10
|
COMETAPI_BASE_URL = "https://api.cometapi.com/v1"
|
|
11
11
|
COMETAPI_DASHBOARD_BASE = "https://api.cometapi.com"
|
|
12
|
+
ACCOUNT_REQUEST_TIMEOUT = 30.0
|
|
13
|
+
ACCOUNT_EXPORT_TIMEOUT = 120.0
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CometClient(openai.OpenAI):
|
|
@@ -44,7 +46,14 @@ class CometClient(openai.OpenAI):
|
|
|
44
46
|
|
|
45
47
|
# -- Account management (access-token auth) --------------------------------
|
|
46
48
|
|
|
47
|
-
def _account_request(
|
|
49
|
+
def _account_request(
|
|
50
|
+
self,
|
|
51
|
+
method: str,
|
|
52
|
+
path: str,
|
|
53
|
+
*,
|
|
54
|
+
params: dict | None = None,
|
|
55
|
+
timeout: float = ACCOUNT_REQUEST_TIMEOUT,
|
|
56
|
+
) -> dict:
|
|
48
57
|
"""Make an authenticated request to a CometAPI account endpoint."""
|
|
49
58
|
if not self._access_token:
|
|
50
59
|
raise openai.OpenAIError(
|
|
@@ -56,6 +65,7 @@ class CometClient(openai.OpenAI):
|
|
|
56
65
|
f"{COMETAPI_DASHBOARD_BASE}{path}",
|
|
57
66
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
58
67
|
params=params,
|
|
68
|
+
timeout=timeout,
|
|
59
69
|
)
|
|
60
70
|
response.raise_for_status()
|
|
61
71
|
return response.json()
|
|
@@ -151,6 +161,7 @@ class CometClient(openai.OpenAI):
|
|
|
151
161
|
start_timestamp: int | None = None,
|
|
152
162
|
end_timestamp: int | None = None,
|
|
153
163
|
group: str | None = None,
|
|
164
|
+
request_id: str | None = None,
|
|
154
165
|
) -> dict:
|
|
155
166
|
"""List the user's usage logs (requires access token)."""
|
|
156
167
|
params: dict[str, Any] = {"p": page, "page_size": page_size}
|
|
@@ -166,6 +177,8 @@ class CometClient(openai.OpenAI):
|
|
|
166
177
|
params["end_timestamp"] = end_timestamp
|
|
167
178
|
if group:
|
|
168
179
|
params["group"] = group
|
|
180
|
+
if request_id:
|
|
181
|
+
params["request_id"] = request_id
|
|
169
182
|
return self._account_request("GET", "/api/log/self", params=params)
|
|
170
183
|
|
|
171
184
|
def search_logs(self, keyword: str) -> dict:
|
|
@@ -235,6 +248,7 @@ class CometClient(openai.OpenAI):
|
|
|
235
248
|
f"{COMETAPI_DASHBOARD_BASE}/api/log/self/export",
|
|
236
249
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
237
250
|
params=params,
|
|
251
|
+
timeout=ACCOUNT_EXPORT_TIMEOUT,
|
|
238
252
|
)
|
|
239
253
|
response.raise_for_status()
|
|
240
254
|
return response.content
|
|
@@ -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
|
|
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 =
|
|
58
|
-
) -> dict | None:
|
|
59
|
-
"""
|
|
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=
|
|
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=
|
|
68
|
-
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
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]
|
|
277
|
-
"
|
|
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":
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|