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.
Files changed (82) hide show
  1. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/CHANGELOG.md +12 -0
  2. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/PKG-INFO +1 -1
  3. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/logs.md +70 -14
  4. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/pyproject.toml +1 -1
  5. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/__init__.py +1 -1
  6. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/logs.py +113 -53
  7. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/constants.py +7 -0
  8. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/conftest.py +5 -2
  9. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_help.py +36 -11
  10. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_logs.py +15 -5
  11. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/uv.lock +1 -1
  12. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  14. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  15. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/workflows/ci.yml +0 -0
  16. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.github/workflows/publish.yml +0 -0
  17. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/.gitignore +0 -0
  18. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/AGENTS.md +0 -0
  19. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/CODE_OF_CONDUCT.md +0 -0
  20. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/CONTRIBUTING.md +0 -0
  21. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/LICENSE +0 -0
  22. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/README.md +0 -0
  23. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/SECURITY.md +0 -0
  24. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/SKILL.md +0 -0
  25. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/README.md +0 -0
  26. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/authentication.md +0 -0
  27. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/account.md +0 -0
  28. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/balance.md +0 -0
  29. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/chat.md +0 -0
  30. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/config.md +0 -0
  31. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/doctor.md +0 -0
  32. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/init.md +0 -0
  33. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/model.md +0 -0
  34. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/models.md +0 -0
  35. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/repl.md +0 -0
  36. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/run.md +0 -0
  37. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/stats.md +0 -0
  38. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/tasks.md +0 -0
  39. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/commands/tokens.md +0 -0
  40. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/configuration.md +0 -0
  41. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/errors.md +0 -0
  42. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/installation.md +0 -0
  43. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/docs/output-formats.md +0 -0
  44. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/skills/live-test/SKILL.md +0 -0
  45. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/app.py +0 -0
  46. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/catalog.py +0 -0
  47. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/client.py +0 -0
  48. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/__init__.py +0 -0
  49. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/account.py +0 -0
  50. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/balance.py +0 -0
  51. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/chat.py +0 -0
  52. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/chat_repl.py +0 -0
  53. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/config_cmd.py +0 -0
  54. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/doctor.py +0 -0
  55. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/model.py +0 -0
  56. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/models.py +0 -0
  57. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/repl.py +0 -0
  58. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/run.py +0 -0
  59. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/stats.py +0 -0
  60. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/tasks.py +0 -0
  61. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/commands/tokens.py +0 -0
  62. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/config.py +0 -0
  63. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/console.py +0 -0
  64. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/errors.py +0 -0
  65. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/formatters.py +0 -0
  66. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/main.py +0 -0
  67. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/src/cometapi_cli/urls.py +0 -0
  68. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/__init__.py +0 -0
  69. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_account.py +0 -0
  70. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_balance.py +0 -0
  71. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_catalog.py +0 -0
  72. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_chat.py +0 -0
  73. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_config.py +0 -0
  74. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_doctor.py +0 -0
  75. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_errors.py +0 -0
  76. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_formatters.py +0 -0
  77. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_model_info.py +0 -0
  78. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_models.py +0 -0
  79. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_run.py +0 -0
  80. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_stats.py +0 -0
  81. {cometapi_cli-0.3.0 → cometapi_cli-0.3.1}/tests/test_tasks.py +0 -0
  82. {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.0
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
- Cost is displayed with 4 decimal places (e.g., `$0.0014`).
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 cost summary card:
166
+ Output shows a request detail card:
164
167
 
165
168
  ```
166
- Request Cost
167
- ┌──────────────┬──────────────────────────┐
168
- │ Field │ Value
169
- ├──────────────┼──────────────────────────┤
170
- │ Request Id │ req-abc-123
171
- Cost $0.0034
172
- Quota (raw) 1,700
173
- RPM 1
174
- TPM 165
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
- JSON output returns the raw stat data:
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
- {"quota": 1700, "rpm": 1, "tpm": 165}
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cometapi-cli"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "CometAPI CLI — official command-line interface for the CometAPI AI gateway"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """CometAPI CLI — professional terminal interface for CometAPI."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -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 extract_items, format_ts, parse_date, quota_to_usd
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 _format_log_detail(log: dict) -> dict:
74
- """Build a rich key-value detail card from a single log entry."""
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
- prompt_tokens = log.get("prompt_tokens", 0)
77
- completion_tokens = log.get("completion_tokens", 0)
78
- quota = log.get("quota", 0)
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"] = log.get("request_id", "") or "—"
82
- info["Response ID"] = log.get("response_id", "") or "—"
83
- info["Time"] = format_ts(log.get("created_at", 0))
84
- info["Model"] = log.get("model_name", "") or "—"
85
- info["Token Name"] = log.get("token_name", "") or "—"
86
- info["Group"] = log.get("group", "") or "—"
87
- info["Stream"] = "Yes" if log.get("is_stream") else "No"
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
- cache_tokens = other.get("cache_tokens", 0)
93
- if cache_tokens:
94
- info["Cache Tokens"] = f"{cache_tokens:,}"
95
-
96
- # Cost
97
- info["Cost (USD)"] = quota_to_usd(quota)
98
- info["Quota (raw)"] = f"{quota:,}"
99
-
100
- # Pricing breakdown from 'other'
101
- model_ratio = other.get("model_ratio")
102
- if model_ratio is not None:
103
- info["Model Ratio"] = model_ratio
104
- completion_ratio = other.get("completion_ratio")
105
- if completion_ratio is not None:
106
- info["Completion Ratio"] = f"{completion_ratio}x"
107
- group_ratio = other.get("group_ratio")
108
- if group_ratio is not None:
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
- use_time = log.get("use_time", 0)
119
- info["Duration"] = f"{use_time:,} ms" if use_time else "—"
120
- frt = other.get("frt")
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
- request_path = other.get("request_path")
126
- if request_path:
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
- if fmt == OutputFormat.JSON:
225
- output(log_entry, fmt)
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
- info = _format_log_detail(log_entry)
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
- if fmt == OutputFormat.JSON:
274
- output(data, fmt)
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": 1200,
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
- assert "MESSAGE" in result.output
56
- assert "--model" in result.output
57
- assert "--json" in result.output
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
- assert "--search" in result.output
64
- assert "--provider" in result.output
65
- assert "--limit" in result.output
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
- assert "--endpoint" in result.output
78
- assert "--input-file" in result.output
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 column displays by checking JSON output."""
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"] == 2
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]["model_name"] == "gpt-5.4"
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 full raw log entry
187
- assert data["model_name"] == "gpt-5.4"
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")
@@ -66,7 +66,7 @@ wheels = [
66
66
 
67
67
  [[package]]
68
68
  name = "cometapi-cli"
69
- version = "0.3.0"
69
+ version = "0.3.1"
70
70
  source = { editable = "." }
71
71
  dependencies = [
72
72
  { name = "openai" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes