cometapi-cli 0.3.0__tar.gz → 0.3.1__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.0 → cometapi_cli-0.3.1}/CHANGELOG.md +12 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/PKG-INFO +1 -1
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/logs.md +70 -14
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/pyproject.toml +1 -1
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/__init__.py +1 -1
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/logs.py +113 -53
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/constants.py +7 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/conftest.py +5 -2
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_help.py +36 -11
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_logs.py +15 -5
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/uv.lock +1 -1
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/workflows/ci.yml +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/workflows/publish.yml +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.gitignore +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/AGENTS.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/CODE_OF_CONDUCT.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/CONTRIBUTING.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/LICENSE +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/README.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/SECURITY.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/SKILL.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/README.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/authentication.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/account.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/balance.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/chat.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/config.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/doctor.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/init.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/model.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/models.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/repl.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/run.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/stats.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/tasks.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/tokens.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/configuration.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/errors.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/installation.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/output-formats.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/skills/live-test/SKILL.md +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/app.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/catalog.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/client.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/__init__.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/account.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/balance.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/chat.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/chat_repl.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/config_cmd.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/doctor.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/model.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/models.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/repl.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/run.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/stats.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/tasks.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/tokens.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/config.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/console.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/errors.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/formatters.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/main.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/urls.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/__init__.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_account.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_balance.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_catalog.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_chat.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_config.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_doctor.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_errors.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_formatters.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_model_info.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_models.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_run.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_stats.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_tasks.py +0 -0
- {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_tokens.py +0 -0
|
@@ -5,6 +5,18 @@ 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.1] — 2026-05-29
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `logs` now emits a consistent field set across all output formats. Previously `--json` dumped the raw API response (no computed `cost`, `other` left as a stringified blob, internal junk fields), while `table`/`yaml`/`markdown` showed a curated card.
|
|
13
|
+
- `logs --request-id` detail duration now reflects true end-to-end latency (`other.total_ms`) instead of the coarse `use_time` seconds value.
|
|
14
|
+
- `logs --request-id` cost now shows 6 decimal places (e.g. `$0.000384`) so sub-cent calls are no longer rounded to `$0.0004`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `logs` JSON/YAML output is now a canonical, typed record (raw numbers, booleans, ISO 8601 time, computed `cost_usd`, exploded `other` fields, normalized `model_price`) with a stable schema; `table`/`markdown` render the same fields as formatted strings.
|
|
19
|
+
|
|
8
20
|
## [0.3.0] — 2026-05-26
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cometapi-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -102,7 +102,10 @@ Internal quota values are converted to USD:
|
|
|
102
102
|
$1.00 USD = 500,000 quota units
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
In the logs list, cost is displayed with 4 decimal places (e.g., `$0.0014`). The
|
|
106
|
+
per-request detail view (`--request-id`) uses 6 decimal places (e.g., `$0.000384`)
|
|
107
|
+
so sub-cent calls are not rounded away, and machine formats expose the raw
|
|
108
|
+
`cost_usd` float.
|
|
106
109
|
|
|
107
110
|
## CSV Export
|
|
108
111
|
|
|
@@ -160,27 +163,80 @@ cometapi logs --request-id req-abc-123
|
|
|
160
163
|
cometapi logs --request-id req-abc-123 --json
|
|
161
164
|
```
|
|
162
165
|
|
|
163
|
-
Output shows a
|
|
166
|
+
Output shows a request detail card:
|
|
164
167
|
|
|
165
168
|
```
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
│ Field
|
|
169
|
-
|
|
170
|
-
│ Request
|
|
171
|
-
│
|
|
172
|
-
│
|
|
173
|
-
│
|
|
174
|
-
│
|
|
175
|
-
|
|
169
|
+
Request Detail
|
|
170
|
+
┌───────────────────┬──────────────────────────────────────────┐
|
|
171
|
+
│ Field │ Value │
|
|
172
|
+
├───────────────────┼──────────────────────────────────────────┤
|
|
173
|
+
│ Request ID │ req-abc-123 │
|
|
174
|
+
│ Response ID │ chatcmpl-xyz-789 │
|
|
175
|
+
│ Time │ 2026-05-29 06:00:21 │
|
|
176
|
+
│ Model │ gpt-5.4 │
|
|
177
|
+
│ Token Name │ Production Key │
|
|
178
|
+
│ Type │ consume │
|
|
179
|
+
│ Stream │ No │
|
|
180
|
+
│ Prompt Tokens │ 18 │
|
|
181
|
+
│ Completion Tokens │ 13 │
|
|
182
|
+
│ Cost (USD) │ $0.000384 │
|
|
183
|
+
│ Quota (raw) │ 192 │
|
|
184
|
+
│ Model Ratio │ 2.5 │
|
|
185
|
+
│ Completion Ratio │ 6x │
|
|
186
|
+
│ Group Ratio │ 0.8 │
|
|
187
|
+
│ Model Price │ default │
|
|
188
|
+
│ Duration │ 3,128 ms │
|
|
189
|
+
│ Endpoint │ /v1/chat/completions │
|
|
190
|
+
└───────────────────┴──────────────────────────────────────────┘
|
|
176
191
|
```
|
|
177
192
|
|
|
178
|
-
|
|
193
|
+
### Unified output schema
|
|
194
|
+
|
|
195
|
+
All formats expose the **same field set** — the difference is only presentation:
|
|
196
|
+
|
|
197
|
+
- `table` / `markdown` — human-friendly labels and formatted strings (`$0.000384`, `Yes`/`No`, `6x`, `3,128 ms`); optional fields are omitted when absent.
|
|
198
|
+
- `json` / `yaml` / `csv` — the canonical record with **raw typed values** (numbers, booleans, ISO 8601 time) and a stable schema (`null` when a value is absent), suitable for scripting.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
cometapi logs --request-id req-abc-123 -f json
|
|
202
|
+
```
|
|
179
203
|
|
|
180
204
|
```json
|
|
181
|
-
{
|
|
205
|
+
{
|
|
206
|
+
"request_id": "req-abc-123",
|
|
207
|
+
"response_id": "chatcmpl-xyz-789",
|
|
208
|
+
"time": "2026-05-29T06:00:21+00:00",
|
|
209
|
+
"type": "consume",
|
|
210
|
+
"model": "gpt-5.4",
|
|
211
|
+
"token_name": "Production Key",
|
|
212
|
+
"username": "google_9550",
|
|
213
|
+
"user_id": 9550,
|
|
214
|
+
"ip": "203.0.113.7",
|
|
215
|
+
"stream": false,
|
|
216
|
+
"prompt_tokens": 18,
|
|
217
|
+
"completion_tokens": 13,
|
|
218
|
+
"cache_tokens": 0,
|
|
219
|
+
"cost_usd": 0.000384,
|
|
220
|
+
"quota": 192,
|
|
221
|
+
"model_ratio": 2.5,
|
|
222
|
+
"completion_ratio": 6,
|
|
223
|
+
"group_ratio": 0.8,
|
|
224
|
+
"model_price": null,
|
|
225
|
+
"cache_ratio": 0.1,
|
|
226
|
+
"duration_ms": 3128,
|
|
227
|
+
"first_token_ms": null,
|
|
228
|
+
"endpoint": "/v1/chat/completions"
|
|
229
|
+
}
|
|
182
230
|
```
|
|
183
231
|
|
|
232
|
+
Notes:
|
|
233
|
+
|
|
234
|
+
- `cost_usd` is `quota / 500000`, rounded to 6 decimals (matches the backend). In the `table`/`markdown` card it is shown as `$0.000384` so sub-cent calls are not rounded away.
|
|
235
|
+
- `duration_ms` uses the request's true end-to-end latency (`other.total_ms`); it falls back to the coarse `use_time` value only when `total_ms` is unavailable.
|
|
236
|
+
- `model_price` is `null` when the backend uses ratio-based pricing (the `-1` sentinel); the `table`/`markdown` card renders this as `default`.
|
|
237
|
+
- `time` is ISO 8601 UTC in machine formats; the card renders `YYYY-MM-DD HH:MM:SS`.
|
|
238
|
+
- `ip` is the client IP recorded by the backend — treat it as sensitive when sharing logs.
|
|
239
|
+
|
|
184
240
|
You can combine `--request-id` with filter options like `--type` and `--model` for additional precision. `--request-id` cannot be used with `--export`.
|
|
185
241
|
|
|
186
242
|
> **Note:** The upstream provider's response ID (e.g., `chatcmpl-DSfol...` from OpenAI) is NOT stored
|
|
@@ -9,7 +9,14 @@ from typing import Annotated
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
11
|
from ..config import get_client
|
|
12
|
-
from ..constants import
|
|
12
|
+
from ..constants import (
|
|
13
|
+
QUOTA_PER_UNIT,
|
|
14
|
+
extract_items,
|
|
15
|
+
format_iso,
|
|
16
|
+
format_ts,
|
|
17
|
+
parse_date,
|
|
18
|
+
quota_to_usd,
|
|
19
|
+
)
|
|
13
20
|
from ..errors import handle_errors
|
|
14
21
|
from ..formatters import OutputFormat, output, resolve_format
|
|
15
22
|
|
|
@@ -70,61 +77,111 @@ def _find_log_by_id(
|
|
|
70
77
|
return None
|
|
71
78
|
|
|
72
79
|
|
|
73
|
-
def
|
|
74
|
-
"""Build
|
|
80
|
+
def _build_log_record(log: dict) -> dict:
|
|
81
|
+
"""Build the canonical, machine-friendly record for a single log entry.
|
|
82
|
+
|
|
83
|
+
Returns a stable schema with raw, typed values (numbers, booleans, ISO 8601
|
|
84
|
+
time). The same field set drives every output format; human-facing formats
|
|
85
|
+
layer display formatting on top via :func:`_record_to_detail_display`.
|
|
86
|
+
Missing values are ``None`` so the schema stays consistent across entries.
|
|
87
|
+
"""
|
|
75
88
|
other = _parse_other(log.get("other"))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
quota = log.get("quota", 0) or 0
|
|
90
|
+
|
|
91
|
+
# model_price == -1 is the backend sentinel for "use ratio-based pricing"
|
|
92
|
+
# (i.e. no fixed per-call price); expose it as null in the canonical record.
|
|
93
|
+
raw_model_price = other.get("model_price")
|
|
94
|
+
model_price = None if raw_model_price == -1 else raw_model_price
|
|
95
|
+
|
|
96
|
+
# Prefer total_ms (true end-to-end latency) over use_time, which the backend
|
|
97
|
+
# records in seconds and is too coarse to be useful as a duration.
|
|
98
|
+
total_ms = other.get("total_ms")
|
|
99
|
+
use_time = log.get("use_time", 0) or 0
|
|
100
|
+
duration_ms = total_ms if total_ms else (use_time or None)
|
|
79
101
|
|
|
102
|
+
frt = other.get("frt")
|
|
103
|
+
user_id = log.get("user_id")
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"request_id": log.get("request_id") or None,
|
|
107
|
+
"response_id": log.get("response_id") or None,
|
|
108
|
+
"time": format_iso(log.get("created_at", 0)),
|
|
109
|
+
"type": _LOG_TYPE_NAMES.get(log.get("type", 0), "unknown"),
|
|
110
|
+
"model": log.get("model_name") or None,
|
|
111
|
+
"token_name": log.get("token_name") or None,
|
|
112
|
+
"username": log.get("username") or None,
|
|
113
|
+
"user_id": user_id if user_id else None,
|
|
114
|
+
"ip": log.get("ip") or None,
|
|
115
|
+
"stream": bool(log.get("is_stream")),
|
|
116
|
+
"prompt_tokens": log.get("prompt_tokens", 0) or 0,
|
|
117
|
+
"completion_tokens": log.get("completion_tokens", 0) or 0,
|
|
118
|
+
"cache_tokens": other.get("cache_tokens", 0) or 0,
|
|
119
|
+
"cost_usd": round(quota / QUOTA_PER_UNIT, 6),
|
|
120
|
+
"quota": quota,
|
|
121
|
+
"model_ratio": other.get("model_ratio"),
|
|
122
|
+
"completion_ratio": other.get("completion_ratio"),
|
|
123
|
+
"group_ratio": other.get("group_ratio"),
|
|
124
|
+
"model_price": model_price,
|
|
125
|
+
"cache_ratio": other.get("cache_ratio"),
|
|
126
|
+
"duration_ms": duration_ms,
|
|
127
|
+
"first_token_ms": frt if frt and frt > 0 else None,
|
|
128
|
+
"endpoint": other.get("request_path") or None,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _record_to_detail_display(rec: dict) -> dict:
|
|
133
|
+
"""Render a canonical log record as a formatted key-value detail card.
|
|
134
|
+
|
|
135
|
+
Mirrors the field set of :func:`_build_log_record` but uses human-friendly
|
|
136
|
+
labels and formatted strings (USD, ``Yes``/``No``, ``ms``). Optional fields
|
|
137
|
+
are omitted when absent to keep the card readable.
|
|
138
|
+
"""
|
|
139
|
+
em_dash = "—"
|
|
80
140
|
info: dict[str, str | int | float] = {}
|
|
81
|
-
info["Request ID"] =
|
|
82
|
-
info["Response ID"] =
|
|
83
|
-
|
|
84
|
-
info["
|
|
85
|
-
info["
|
|
86
|
-
info["
|
|
87
|
-
|
|
141
|
+
info["Request ID"] = rec["request_id"] or em_dash
|
|
142
|
+
info["Response ID"] = rec["response_id"] or em_dash
|
|
143
|
+
iso = rec["time"]
|
|
144
|
+
info["Time"] = iso[:19].replace("T", " ") if iso else em_dash
|
|
145
|
+
info["Model"] = rec["model"] or em_dash
|
|
146
|
+
info["Token Name"] = rec["token_name"] or em_dash
|
|
147
|
+
if rec["username"]:
|
|
148
|
+
info["Username"] = rec["username"]
|
|
149
|
+
if rec["user_id"] is not None:
|
|
150
|
+
info["User ID"] = rec["user_id"]
|
|
151
|
+
if rec["ip"]:
|
|
152
|
+
info["IP"] = rec["ip"]
|
|
153
|
+
info["Type"] = rec["type"]
|
|
154
|
+
info["Stream"] = "Yes" if rec["stream"] else "No"
|
|
88
155
|
|
|
89
156
|
# Tokens
|
|
90
|
-
info["Prompt Tokens"] = f"{prompt_tokens:,}"
|
|
91
|
-
info["Completion Tokens"] = f"{completion_tokens:,}"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
info["
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
info["
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
info["Group Ratio"] = group_ratio
|
|
110
|
-
model_price = other.get("model_price")
|
|
111
|
-
if model_price is not None:
|
|
112
|
-
info["Model Price"] = "default" if model_price == -1 else model_price
|
|
113
|
-
cache_ratio = other.get("cache_ratio")
|
|
114
|
-
if cache_ratio is not None:
|
|
115
|
-
info["Cache Ratio"] = cache_ratio
|
|
157
|
+
info["Prompt Tokens"] = f"{rec['prompt_tokens']:,}"
|
|
158
|
+
info["Completion Tokens"] = f"{rec['completion_tokens']:,}"
|
|
159
|
+
if rec["cache_tokens"]:
|
|
160
|
+
info["Cache Tokens"] = f"{rec['cache_tokens']:,}"
|
|
161
|
+
|
|
162
|
+
# Cost (6 decimals so sub-cent calls are not rounded away)
|
|
163
|
+
info["Cost (USD)"] = f"${rec['cost_usd']:,.6f}"
|
|
164
|
+
info["Quota (raw)"] = f"{rec['quota']:,}"
|
|
165
|
+
|
|
166
|
+
# Pricing breakdown
|
|
167
|
+
if rec["model_ratio"] is not None:
|
|
168
|
+
info["Model Ratio"] = rec["model_ratio"]
|
|
169
|
+
if rec["completion_ratio"] is not None:
|
|
170
|
+
info["Completion Ratio"] = f"{rec['completion_ratio']}x"
|
|
171
|
+
if rec["group_ratio"] is not None:
|
|
172
|
+
info["Group Ratio"] = rec["group_ratio"]
|
|
173
|
+
info["Model Price"] = "default" if rec["model_price"] is None else rec["model_price"]
|
|
174
|
+
if rec["cache_ratio"] is not None:
|
|
175
|
+
info["Cache Ratio"] = rec["cache_ratio"]
|
|
116
176
|
|
|
117
177
|
# Timing
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if frt and frt > 0:
|
|
122
|
-
info["First Token"] = f"{frt:,} ms"
|
|
178
|
+
info["Duration"] = f"{rec['duration_ms']:,} ms" if rec["duration_ms"] else em_dash
|
|
179
|
+
if rec["first_token_ms"]:
|
|
180
|
+
info["First Token"] = f"{rec['first_token_ms']:,} ms"
|
|
123
181
|
|
|
124
182
|
# Path
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
info["Endpoint"] = request_path
|
|
183
|
+
if rec["endpoint"]:
|
|
184
|
+
info["Endpoint"] = rec["endpoint"]
|
|
128
185
|
|
|
129
186
|
return info
|
|
130
187
|
|
|
@@ -221,12 +278,13 @@ def logs(
|
|
|
221
278
|
)
|
|
222
279
|
raise typer.Exit(code=1)
|
|
223
280
|
|
|
224
|
-
|
|
225
|
-
|
|
281
|
+
record = _build_log_record(log_entry)
|
|
282
|
+
# Machine formats get raw typed values; human formats get the formatted card.
|
|
283
|
+
if fmt in (OutputFormat.JSON, OutputFormat.YAML, OutputFormat.CSV):
|
|
284
|
+
output(record, fmt)
|
|
226
285
|
return
|
|
227
286
|
|
|
228
|
-
|
|
229
|
-
output(info, fmt, title="Request Detail")
|
|
287
|
+
output(_record_to_detail_display(record), fmt, title="Request Detail")
|
|
230
288
|
return
|
|
231
289
|
|
|
232
290
|
# Server-side CSV export — write raw bytes to stdout and return early
|
|
@@ -270,8 +328,10 @@ def logs(
|
|
|
270
328
|
|
|
271
329
|
data = extract_items(resp)
|
|
272
330
|
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
# Machine formats emit the full canonical record per entry (typed values,
|
|
332
|
+
# incl. cost_usd); table/markdown/csv keep the compact summary rows.
|
|
333
|
+
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
|
|
334
|
+
output([_build_log_record(log) for log in data], fmt)
|
|
275
335
|
return
|
|
276
336
|
|
|
277
337
|
if not data:
|
|
@@ -24,6 +24,13 @@ def format_ts(ts: int, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
|
24
24
|
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime(fmt)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def format_iso(ts: int) -> str | None:
|
|
28
|
+
"""Format a Unix timestamp as an ISO 8601 UTC string, or None when absent."""
|
|
29
|
+
if not ts:
|
|
30
|
+
return None
|
|
31
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
def parse_date(value: str, label: str) -> int:
|
|
28
35
|
"""Parse a date string into a Unix timestamp (UTC).
|
|
29
36
|
|
|
@@ -166,14 +166,17 @@ def mock_client():
|
|
|
166
166
|
"type": 2,
|
|
167
167
|
"model_name": "gpt-5.4",
|
|
168
168
|
"token_name": "Production Key",
|
|
169
|
+
"username": "google_9550",
|
|
170
|
+
"user_id": 9550,
|
|
171
|
+
"ip": "203.0.113.7",
|
|
169
172
|
"quota": 150,
|
|
170
173
|
"prompt_tokens": 120,
|
|
171
174
|
"completion_tokens": 45,
|
|
172
|
-
"use_time":
|
|
175
|
+
"use_time": 1,
|
|
173
176
|
"is_stream": True,
|
|
174
177
|
"request_id": "req-abc-001",
|
|
175
178
|
"response_id": "chatcmpl-xyz-001",
|
|
176
|
-
"other": '{"model_ratio":0.625,"completion_ratio":8,"group_ratio":0.8,"model_price":-1,"frt":120}',
|
|
179
|
+
"other": '{"model_ratio":0.625,"completion_ratio":8,"group_ratio":0.8,"model_price":-1,"frt":120,"total_ms":1200,"request_path":"/v1/chat/completions"}',
|
|
177
180
|
},
|
|
178
181
|
{
|
|
179
182
|
"id": 101,
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"""Tests for CLI version and help output."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
from cometapi_cli import __version__
|
|
4
6
|
|
|
7
|
+
ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def clean_output(output: str) -> str:
|
|
11
|
+
return ANSI_RE.sub("", output)
|
|
12
|
+
|
|
5
13
|
|
|
6
14
|
def test_version(cli_runner):
|
|
7
15
|
result = cli_runner("--version")
|
|
@@ -46,42 +54,59 @@ def test_subcommand_help_short_flag(cli_runner):
|
|
|
46
54
|
"""Test that -h works for subcommands."""
|
|
47
55
|
result = cli_runner("balance", "-h")
|
|
48
56
|
assert result.exit_code == 0
|
|
49
|
-
assert "--source" in result.output
|
|
57
|
+
assert "--source" in clean_output(result.output)
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
def test_chat_help(cli_runner):
|
|
53
61
|
result = cli_runner("chat", "--help")
|
|
54
62
|
assert result.exit_code == 0
|
|
55
|
-
|
|
56
|
-
assert "
|
|
57
|
-
assert "--
|
|
63
|
+
output = clean_output(result.output)
|
|
64
|
+
assert "MESSAGE" in output
|
|
65
|
+
assert "--model" in output
|
|
66
|
+
assert "--json" in output
|
|
58
67
|
|
|
59
68
|
|
|
60
69
|
def test_models_help(cli_runner):
|
|
61
70
|
result = cli_runner("models", "--help")
|
|
62
71
|
assert result.exit_code == 0
|
|
63
|
-
|
|
64
|
-
assert "--
|
|
65
|
-
assert "--
|
|
72
|
+
output = clean_output(result.output)
|
|
73
|
+
assert "--search" in output
|
|
74
|
+
assert "--provider" in output
|
|
75
|
+
assert "--limit" in output
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
def test_model_info_help(cli_runner):
|
|
69
79
|
result = cli_runner("model", "info", "--help")
|
|
70
80
|
assert result.exit_code == 0
|
|
71
|
-
assert "MODEL_ID" in result.output
|
|
81
|
+
assert "MODEL_ID" in clean_output(result.output)
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
def test_run_help(cli_runner):
|
|
75
85
|
result = cli_runner("run", "--help")
|
|
76
86
|
assert result.exit_code == 0
|
|
77
|
-
|
|
78
|
-
assert "--
|
|
87
|
+
output = clean_output(result.output)
|
|
88
|
+
assert "--endpoint" in output
|
|
89
|
+
assert "--input-file" in output
|
|
79
90
|
|
|
80
91
|
|
|
81
92
|
def test_doctor_help(cli_runner):
|
|
82
93
|
result = cli_runner("doctor", "--help")
|
|
83
94
|
assert result.exit_code == 0
|
|
84
|
-
assert "--json" in result.output
|
|
95
|
+
assert "--json" in clean_output(result.output)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_help_flag_assertions_tolerate_color(cli_runner):
|
|
99
|
+
cases = [
|
|
100
|
+
(("balance", "-h"), "--source"),
|
|
101
|
+
(("chat", "--help"), "--model"),
|
|
102
|
+
(("models", "--help"), "--search"),
|
|
103
|
+
(("run", "--help"), "--endpoint"),
|
|
104
|
+
(("doctor", "--help"), "--json"),
|
|
105
|
+
]
|
|
106
|
+
for args, expected in cases:
|
|
107
|
+
result = cli_runner(*args, color=True)
|
|
108
|
+
assert result.exit_code == 0
|
|
109
|
+
assert expected in clean_output(result.output)
|
|
85
110
|
|
|
86
111
|
|
|
87
112
|
def test_config_help(cli_runner):
|
|
@@ -15,11 +15,11 @@ class TestLogsTable:
|
|
|
15
15
|
patched_client.list_logs.assert_called_once()
|
|
16
16
|
|
|
17
17
|
def test_logs_type_column(self, cli_runner, patched_client):
|
|
18
|
-
"""Verify type
|
|
18
|
+
"""Verify type is exposed by name in the canonical JSON record."""
|
|
19
19
|
result = cli_runner("logs", "--json")
|
|
20
20
|
assert result.exit_code == 0
|
|
21
21
|
data = json.loads(result.output)
|
|
22
|
-
assert data[0]["type"] ==
|
|
22
|
+
assert data[0]["type"] == "consume"
|
|
23
23
|
|
|
24
24
|
def test_logs_filter_model(self, cli_runner, patched_client):
|
|
25
25
|
result = cli_runner("logs", "--model", "gpt-5.4")
|
|
@@ -153,8 +153,10 @@ class TestLogsJSON:
|
|
|
153
153
|
data = json.loads(result.output)
|
|
154
154
|
assert isinstance(data, list)
|
|
155
155
|
assert len(data) == 2
|
|
156
|
-
assert data[0]["
|
|
156
|
+
assert data[0]["model"] == "gpt-5.4"
|
|
157
157
|
assert data[0]["prompt_tokens"] == 120
|
|
158
|
+
# cost_usd is computed and present in every format now
|
|
159
|
+
assert data[0]["cost_usd"] == 0.0003
|
|
158
160
|
|
|
159
161
|
def test_logs_format_yaml(self, cli_runner, patched_client):
|
|
160
162
|
result = cli_runner("logs", "--format", "yaml")
|
|
@@ -183,12 +185,20 @@ class TestLogsRequestId:
|
|
|
183
185
|
result = cli_runner("logs", "--request-id", "req-abc-001", "--json")
|
|
184
186
|
assert result.exit_code == 0
|
|
185
187
|
data = json.loads(result.output)
|
|
186
|
-
# JSON returns the
|
|
187
|
-
assert data["
|
|
188
|
+
# JSON returns the canonical typed record (same field set as table/yaml)
|
|
189
|
+
assert data["model"] == "gpt-5.4"
|
|
188
190
|
assert data["prompt_tokens"] == 120
|
|
189
191
|
assert data["completion_tokens"] == 45
|
|
190
192
|
assert data["quota"] == 150
|
|
191
193
|
assert data["request_id"] == "req-abc-001"
|
|
194
|
+
# Computed cost is present (was missing from raw JSON before)
|
|
195
|
+
assert data["cost_usd"] == 0.0003
|
|
196
|
+
# Time is ISO 8601, not an epoch int
|
|
197
|
+
assert data["time"].startswith("2023-11-16T")
|
|
198
|
+
# Duration comes from total_ms, not the coarse use_time seconds field
|
|
199
|
+
assert data["duration_ms"] == 1200
|
|
200
|
+
# model_price == -1 sentinel is normalized to null
|
|
201
|
+
assert data["model_price"] is None
|
|
192
202
|
|
|
193
203
|
def test_logs_request_id_not_found(self, cli_runner, patched_client):
|
|
194
204
|
result = cli_runner("logs", "--request-id", "nonexistent-id")
|
|
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
|
|
File without changes
|