cli-consumption 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.
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/CHANGELOG.md +15 -1
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/PKG-INFO +1 -1
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/pyproject.toml +1 -1
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/opencode.py +253 -84
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/registry.py +8 -5
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/.gitignore +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/LICENSE +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/NOTICE +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/README.md +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/__init__.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/__main__.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/__init__.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/_shared.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/aider.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/amazon_q.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/amp.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/base.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/claude.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/cline.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/codex.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/continue_cli.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/copilot.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/crush.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/cursor.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/gemini.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/goose.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/grok.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/kilo.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/kimi.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/mistral_vibe.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/openhands.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/pi.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/plandex.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/qwen.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/api.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/cli.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/dashboard.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/dashboard_calculations.js +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/exporting.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/__init__.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/env.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/__init__.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/v0001_baseline.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/v0002_minimize_subagents.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/v0003_canonical_timestamps.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/v0004_subagent_scope_freshness.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/v0005_sync_receipts.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/models.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/py.typed +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/qualifications.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/reporting.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/retention.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/schema.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/storage.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/sync.py +0 -0
- {cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/timestamps.py +0 -0
|
@@ -6,6 +6,19 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-08-31
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Version releases now attach the validated wheel and source distribution to an
|
|
14
|
+
automatically generated GitHub Release after PyPI publication succeeds.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- OpenCode 1.18.23 SQLite stores now extract model calls and token usage from
|
|
19
|
+
`message.data` and tool calls from `part.data`; incomplete current schemas are
|
|
20
|
+
reported as incompatible instead of silently producing zero usage.
|
|
21
|
+
|
|
9
22
|
## [0.3.1] - 2026-08-31
|
|
10
23
|
|
|
11
24
|
### Changed
|
|
@@ -102,7 +115,8 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
102
115
|
|
|
103
116
|
- Refreshed the provider guide for the first minor release ([#26]).
|
|
104
117
|
|
|
105
|
-
[Unreleased]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.
|
|
118
|
+
[Unreleased]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.2...HEAD
|
|
119
|
+
[0.3.2]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.1...v0.3.2
|
|
106
120
|
[0.3.1]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.0...v0.3.1
|
|
107
121
|
[0.3.0]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.2.1...v0.3.0
|
|
108
122
|
[0.2.1]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.2.0...v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-consumption
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Analyze and consolidate AI coding CLI consumption across machines.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Guillaume-Lombardo/cli-consumption
|
|
6
6
|
Project-URL: Documentation, https://github.com/Guillaume-Lombardo/cli-consumption#readme
|
|
@@ -39,6 +39,15 @@ from cli_consumption.adapters.base import UnsupportedProviderFormat
|
|
|
39
39
|
from cli_consumption.models import Snapshot, empty_tokens
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class _Part:
|
|
44
|
+
external_id: str
|
|
45
|
+
kind: str
|
|
46
|
+
created_at: datetime | None
|
|
47
|
+
updated_at: datetime | None
|
|
48
|
+
data: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
42
51
|
@dataclass(slots=True)
|
|
43
52
|
class _Message:
|
|
44
53
|
external_id: str
|
|
@@ -47,6 +56,7 @@ class _Message:
|
|
|
47
56
|
created_at: datetime | None
|
|
48
57
|
updated_at: datetime | None
|
|
49
58
|
data: dict[str, Any]
|
|
59
|
+
parts: list[_Part]
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
@dataclass(slots=True)
|
|
@@ -123,6 +133,8 @@ class OpenCodeAdapter:
|
|
|
123
133
|
_mapping(message.data.get("time")).get("completed")
|
|
124
134
|
)
|
|
125
135
|
timestamps.extend((timestamp, completed_at))
|
|
136
|
+
for part in message.parts:
|
|
137
|
+
timestamps.extend((part.created_at, part.updated_at))
|
|
126
138
|
|
|
127
139
|
if message.kind == "user":
|
|
128
140
|
if active is not None:
|
|
@@ -142,48 +154,53 @@ class OpenCodeAdapter:
|
|
|
142
154
|
**empty_tokens(),
|
|
143
155
|
}
|
|
144
156
|
turn_models[active] = set()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if message.kind == "model-switched":
|
|
157
|
+
elif message.kind == "model-switched":
|
|
148
158
|
effective_model = _model(message.data.get("model"))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if message.kind == "compaction":
|
|
159
|
+
elif message.kind == "compaction":
|
|
152
160
|
compactions.append((active, timestamp))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
continue
|
|
178
|
-
for part in content:
|
|
179
|
-
if not isinstance(part, dict) or part.get("type") != "tool":
|
|
161
|
+
elif message.kind == "assistant":
|
|
162
|
+
model = _message_model(message.data) or effective_model or "unknown"
|
|
163
|
+
effective_model = model if model != "unknown" else effective_model
|
|
164
|
+
tokens = _usage(message.data.get("tokens"))
|
|
165
|
+
calls.append((active, timestamp, model, tokens))
|
|
166
|
+
turn = turns.get(active or "")
|
|
167
|
+
if turn:
|
|
168
|
+
turn["model_calls"] += 1
|
|
169
|
+
turn_models[active or ""].add(model)
|
|
170
|
+
_add_tokens(turn, tokens)
|
|
171
|
+
turn["ended_at"] = (
|
|
172
|
+
_iso(completed_at or timestamp) or turn["ended_at"]
|
|
173
|
+
)
|
|
174
|
+
if isinstance(message.data.get("error"), dict):
|
|
175
|
+
turn["status"] = "aborted"
|
|
176
|
+
elif (
|
|
177
|
+
completed_at is not None
|
|
178
|
+
or _label(message.data.get("finish"), 255)
|
|
179
|
+
) and turn["status"] != "aborted":
|
|
180
|
+
turn["status"] = "completed"
|
|
181
|
+
|
|
182
|
+
for part in message.parts:
|
|
183
|
+
if part.kind == "compaction":
|
|
184
|
+
compactions.append((active, part.created_at or timestamp))
|
|
180
185
|
continue
|
|
181
|
-
|
|
186
|
+
if part.kind != "tool":
|
|
187
|
+
continue
|
|
188
|
+
name = _label(part.data.get("tool") or part.data.get("name"), 512)
|
|
182
189
|
if not name:
|
|
183
190
|
continue
|
|
184
|
-
|
|
191
|
+
state_time = _mapping(_mapping(part.data.get("state")).get("time"))
|
|
192
|
+
part_time = _mapping(part.data.get("time"))
|
|
185
193
|
tools.append(
|
|
186
|
-
(
|
|
194
|
+
(
|
|
195
|
+
active,
|
|
196
|
+
part.created_at
|
|
197
|
+
or _timestamp(
|
|
198
|
+
part_time.get("created") or part_time.get("start")
|
|
199
|
+
)
|
|
200
|
+
or _timestamp(state_time.get("start"))
|
|
201
|
+
or timestamp,
|
|
202
|
+
name,
|
|
203
|
+
)
|
|
187
204
|
)
|
|
188
205
|
|
|
189
206
|
ended_at = max(
|
|
@@ -295,25 +312,59 @@ def _read_database(
|
|
|
295
312
|
connection = manager.__enter__()
|
|
296
313
|
connection.row_factory = sqlite3.Row
|
|
297
314
|
session_columns = _columns(connection, "session")
|
|
298
|
-
message_columns = _columns(connection, "session_message")
|
|
299
315
|
required_session = {"id", "time_created", "time_updated"}
|
|
300
|
-
|
|
301
|
-
"id",
|
|
302
|
-
"session_id",
|
|
303
|
-
"type",
|
|
304
|
-
"seq",
|
|
305
|
-
"time_created",
|
|
306
|
-
"time_updated",
|
|
307
|
-
"data",
|
|
308
|
-
}
|
|
309
|
-
if (
|
|
310
|
-
not required_session <= session_columns
|
|
311
|
-
or not required_message <= message_columns
|
|
312
|
-
):
|
|
316
|
+
if not required_session <= session_columns:
|
|
313
317
|
raise UnsupportedProviderFormat(
|
|
314
318
|
f"Unsupported OpenCode database schema: {path}"
|
|
315
319
|
)
|
|
316
|
-
|
|
320
|
+
|
|
321
|
+
message_columns = _columns(connection, "message")
|
|
322
|
+
part_columns = _columns(connection, "part")
|
|
323
|
+
current_schema_present = bool(message_columns or part_columns)
|
|
324
|
+
if current_schema_present:
|
|
325
|
+
required_message = {
|
|
326
|
+
"id",
|
|
327
|
+
"session_id",
|
|
328
|
+
"time_created",
|
|
329
|
+
"time_updated",
|
|
330
|
+
"data",
|
|
331
|
+
}
|
|
332
|
+
required_part = {
|
|
333
|
+
"id",
|
|
334
|
+
"message_id",
|
|
335
|
+
"session_id",
|
|
336
|
+
"time_created",
|
|
337
|
+
"time_updated",
|
|
338
|
+
"data",
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
not required_message <= message_columns
|
|
342
|
+
or not required_part <= part_columns
|
|
343
|
+
):
|
|
344
|
+
raise UnsupportedProviderFormat(
|
|
345
|
+
f"Unsupported OpenCode database schema: {path}"
|
|
346
|
+
)
|
|
347
|
+
ensure_provider_sqlite_fields(
|
|
348
|
+
connection, [("message", "data"), ("part", "data")]
|
|
349
|
+
)
|
|
350
|
+
read_messages = _read_current_messages
|
|
351
|
+
else:
|
|
352
|
+
projection_columns = _columns(connection, "session_message")
|
|
353
|
+
required_projection = {
|
|
354
|
+
"id",
|
|
355
|
+
"session_id",
|
|
356
|
+
"type",
|
|
357
|
+
"seq",
|
|
358
|
+
"time_created",
|
|
359
|
+
"time_updated",
|
|
360
|
+
"data",
|
|
361
|
+
}
|
|
362
|
+
if not required_projection <= projection_columns:
|
|
363
|
+
raise UnsupportedProviderFormat(
|
|
364
|
+
f"Unsupported OpenCode database schema: {path}"
|
|
365
|
+
)
|
|
366
|
+
ensure_provider_sqlite_fields(connection, [("session_message", "data")])
|
|
367
|
+
read_messages = _read_projection_messages
|
|
317
368
|
|
|
318
369
|
directory = "directory" if "directory" in session_columns else "NULL"
|
|
319
370
|
rows = budget.rows(
|
|
@@ -329,39 +380,9 @@ def _read_database(
|
|
|
329
380
|
if not external_id:
|
|
330
381
|
malformed += 1
|
|
331
382
|
continue
|
|
332
|
-
message_rows = budget.rows(
|
|
333
|
-
connection.execute(
|
|
334
|
-
"SELECT id, type, seq, time_created, time_updated, data "
|
|
335
|
-
"FROM session_message WHERE session_id = ? ORDER BY seq, id",
|
|
336
|
-
(row["id"],),
|
|
337
|
-
)
|
|
338
|
-
)
|
|
339
383
|
digest = hashlib.sha256()
|
|
340
|
-
messages
|
|
341
|
-
|
|
342
|
-
budget.json_field(message_row["data"])
|
|
343
|
-
digest.update(str(tuple(message_row)).encode())
|
|
344
|
-
message_id = _label(message_row["id"], 512)
|
|
345
|
-
kind = _label(message_row["type"], 64)
|
|
346
|
-
try:
|
|
347
|
-
data = json.loads(message_row["data"])
|
|
348
|
-
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
|
|
349
|
-
malformed += 1
|
|
350
|
-
continue
|
|
351
|
-
if not message_id or not kind or not isinstance(data, dict):
|
|
352
|
-
malformed += 1
|
|
353
|
-
continue
|
|
354
|
-
sequence = _counter(message_row["seq"])
|
|
355
|
-
messages.append(
|
|
356
|
-
_Message(
|
|
357
|
-
message_id,
|
|
358
|
-
kind,
|
|
359
|
-
sequence,
|
|
360
|
-
_timestamp(message_row["time_created"]),
|
|
361
|
-
_timestamp(message_row["time_updated"]),
|
|
362
|
-
data,
|
|
363
|
-
)
|
|
364
|
-
)
|
|
384
|
+
messages, invalid = read_messages(connection, row["id"], budget, digest)
|
|
385
|
+
malformed += invalid
|
|
365
386
|
conversations.append(
|
|
366
387
|
_Conversation(
|
|
367
388
|
machine=machine,
|
|
@@ -383,9 +404,148 @@ def _read_database(
|
|
|
383
404
|
manager.__exit__(None, None, None)
|
|
384
405
|
|
|
385
406
|
|
|
407
|
+
def _read_current_messages(
|
|
408
|
+
connection: sqlite3.Connection,
|
|
409
|
+
session_id: object,
|
|
410
|
+
budget: ProviderInputBudget,
|
|
411
|
+
digest: Any,
|
|
412
|
+
) -> tuple[list[_Message], int]:
|
|
413
|
+
message_rows = budget.rows(
|
|
414
|
+
connection.execute(
|
|
415
|
+
"SELECT id, time_created, time_updated, data "
|
|
416
|
+
"FROM message WHERE session_id = ? ORDER BY time_created, id",
|
|
417
|
+
(session_id,),
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
messages: list[_Message] = []
|
|
421
|
+
messages_by_id: dict[str, _Message] = {}
|
|
422
|
+
malformed = 0
|
|
423
|
+
for sequence, row in enumerate(message_rows, 1):
|
|
424
|
+
budget.json_field(row["data"])
|
|
425
|
+
digest.update(str(tuple(row)).encode())
|
|
426
|
+
message_id = _label(row["id"], 512)
|
|
427
|
+
data = _json_object(row["data"])
|
|
428
|
+
kind = _label(data.get("role"), 64) if data is not None else None
|
|
429
|
+
if not message_id or data is None or kind not in {"user", "assistant"}:
|
|
430
|
+
malformed += 1
|
|
431
|
+
continue
|
|
432
|
+
message = _Message(
|
|
433
|
+
message_id,
|
|
434
|
+
kind,
|
|
435
|
+
sequence,
|
|
436
|
+
_timestamp(row["time_created"]),
|
|
437
|
+
_timestamp(row["time_updated"]),
|
|
438
|
+
data,
|
|
439
|
+
[],
|
|
440
|
+
)
|
|
441
|
+
messages.append(message)
|
|
442
|
+
messages_by_id[message_id] = message
|
|
443
|
+
|
|
444
|
+
part_rows = budget.rows(
|
|
445
|
+
connection.execute(
|
|
446
|
+
"SELECT id, message_id, time_created, time_updated, data "
|
|
447
|
+
"FROM part WHERE session_id = ? ORDER BY message_id, id",
|
|
448
|
+
(session_id,),
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
for row in part_rows:
|
|
452
|
+
budget.json_field(row["data"])
|
|
453
|
+
digest.update(str(tuple(row)).encode())
|
|
454
|
+
part_id = _label(row["id"], 512)
|
|
455
|
+
message_id = _label(row["message_id"], 512)
|
|
456
|
+
data = _json_object(row["data"])
|
|
457
|
+
kind = _label(data.get("type"), 64) if data is not None else None
|
|
458
|
+
message = messages_by_id.get(message_id or "")
|
|
459
|
+
if not part_id or not message_id or data is None or not kind or message is None:
|
|
460
|
+
malformed += 1
|
|
461
|
+
continue
|
|
462
|
+
message.parts.append(
|
|
463
|
+
_Part(
|
|
464
|
+
part_id,
|
|
465
|
+
kind,
|
|
466
|
+
_timestamp(row["time_created"]),
|
|
467
|
+
_timestamp(row["time_updated"]),
|
|
468
|
+
data,
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
return messages, malformed
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _read_projection_messages(
|
|
475
|
+
connection: sqlite3.Connection,
|
|
476
|
+
session_id: object,
|
|
477
|
+
budget: ProviderInputBudget,
|
|
478
|
+
digest: Any,
|
|
479
|
+
) -> tuple[list[_Message], int]:
|
|
480
|
+
rows = budget.rows(
|
|
481
|
+
connection.execute(
|
|
482
|
+
"SELECT id, type, seq, time_created, time_updated, data "
|
|
483
|
+
"FROM session_message WHERE session_id = ? ORDER BY seq, id",
|
|
484
|
+
(session_id,),
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
messages: list[_Message] = []
|
|
488
|
+
malformed = 0
|
|
489
|
+
for row in rows:
|
|
490
|
+
budget.json_field(row["data"])
|
|
491
|
+
digest.update(str(tuple(row)).encode())
|
|
492
|
+
message_id = _label(row["id"], 512)
|
|
493
|
+
kind = _label(row["type"], 64)
|
|
494
|
+
data = _json_object(row["data"])
|
|
495
|
+
if not message_id or not kind or data is None:
|
|
496
|
+
malformed += 1
|
|
497
|
+
continue
|
|
498
|
+
messages.append(
|
|
499
|
+
_Message(
|
|
500
|
+
message_id,
|
|
501
|
+
kind,
|
|
502
|
+
_counter(row["seq"]),
|
|
503
|
+
_timestamp(row["time_created"]),
|
|
504
|
+
_timestamp(row["time_updated"]),
|
|
505
|
+
data,
|
|
506
|
+
_projection_parts(message_id, data),
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
return messages, malformed
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _projection_parts(message_id: str, data: dict[str, Any]) -> list[_Part]:
|
|
513
|
+
content = data.get("content")
|
|
514
|
+
if not isinstance(content, list):
|
|
515
|
+
return []
|
|
516
|
+
result: list[_Part] = []
|
|
517
|
+
for sequence, value in enumerate(content, 1):
|
|
518
|
+
if not isinstance(value, dict):
|
|
519
|
+
continue
|
|
520
|
+
kind = _label(value.get("type"), 64)
|
|
521
|
+
if not kind:
|
|
522
|
+
continue
|
|
523
|
+
part_time = _mapping(value.get("time"))
|
|
524
|
+
result.append(
|
|
525
|
+
_Part(
|
|
526
|
+
_label(value.get("id"), 512) or f"{message_id}:part:{sequence}",
|
|
527
|
+
kind,
|
|
528
|
+
_timestamp(part_time.get("created") or part_time.get("start")),
|
|
529
|
+
_timestamp(part_time.get("completed") or part_time.get("end")),
|
|
530
|
+
value,
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
return result
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _json_object(value: object) -> dict[str, Any] | None:
|
|
537
|
+
if not isinstance(value, str | bytes | bytearray):
|
|
538
|
+
return None
|
|
539
|
+
try:
|
|
540
|
+
parsed = json.loads(value)
|
|
541
|
+
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
|
|
542
|
+
return None
|
|
543
|
+
return parsed if isinstance(parsed, dict) else None
|
|
544
|
+
|
|
545
|
+
|
|
386
546
|
def _rank(value: _Conversation) -> tuple[int, datetime, str]:
|
|
387
547
|
return (
|
|
388
|
-
len(value.messages),
|
|
548
|
+
sum(1 + len(message.parts) for message in value.messages),
|
|
389
549
|
value.updated_at or datetime.min.replace(tzinfo=UTC),
|
|
390
550
|
value.digest,
|
|
391
551
|
)
|
|
@@ -426,6 +586,15 @@ def _model(value: object) -> str | None:
|
|
|
426
586
|
return identifier
|
|
427
587
|
|
|
428
588
|
|
|
589
|
+
def _message_model(data: dict[str, Any]) -> str | None:
|
|
590
|
+
return _model(data.get("model")) or _model(
|
|
591
|
+
{
|
|
592
|
+
"providerID": data.get("providerID"),
|
|
593
|
+
"modelID": data.get("modelID"),
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
429
598
|
def _finish_turn(turn: dict[str, Any], fallback: datetime | None) -> None:
|
|
430
599
|
turn["ended_at"] = turn["ended_at"] or _iso(fallback)
|
|
431
600
|
start, end = _timestamp(turn["started_at"]), _timestamp(turn["ended_at"])
|
|
@@ -98,10 +98,12 @@ def _qualification(
|
|
|
98
98
|
format: str,
|
|
99
99
|
provenance: str,
|
|
100
100
|
limitations: str,
|
|
101
|
+
*,
|
|
102
|
+
qualified_on: str = _QUALIFIED_ON,
|
|
101
103
|
) -> AdapterQualification:
|
|
102
104
|
return AdapterQualification(
|
|
103
105
|
version=version,
|
|
104
|
-
qualified_on=
|
|
106
|
+
qualified_on=qualified_on,
|
|
105
107
|
format=format,
|
|
106
108
|
fixture=f"tests/test_{name.replace('-', '_')}_adapter.py",
|
|
107
109
|
provenance=provenance,
|
|
@@ -358,10 +360,11 @@ ADAPTER_SPECS = (
|
|
|
358
360
|
token_semantics="additive",
|
|
359
361
|
qualification=_qualification(
|
|
360
362
|
"opencode",
|
|
361
|
-
"SQLite v2",
|
|
362
|
-
"SQLite session
|
|
363
|
-
"https://github.com/anomalyco/opencode/tree/
|
|
364
|
-
"No
|
|
363
|
+
"CLI 1.18.23 / SQLite v2",
|
|
364
|
+
"SQLite session plus current message/part or projection records",
|
|
365
|
+
"https://github.com/anomalyco/opencode/tree/v1.18.23",
|
|
366
|
+
"No pre-v2 JSON, child sessions, context windows, or costs.",
|
|
367
|
+
qualified_on="2026-08-31",
|
|
365
368
|
),
|
|
366
369
|
),
|
|
367
370
|
AdapterSpec(
|
|
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
|
{cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/continue_cli.py
RENAMED
|
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
|
{cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/adapters/mistral_vibe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/dashboard_calculations.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_consumption-0.3.1 → cli_consumption-0.3.2}/src/cli_consumption/migrations/versions/__init__.py
RENAMED
|
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
|