cdx-manager 0.24.2 → 0.24.3
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.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_24_3.md +13 -0
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/commands/launch.py +4 -4
- package/src/commands/runs.py +2 -0
- package/src/commands/status.py +2 -1
- package/src/run_command.py +7 -6
- package/src/run_registry.py +1 -0
- package/src/run_usage.py +38 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.24.
|
|
1
|
+
0.24.3
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# CDX Manager 0.24.3
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-29
|
|
4
|
+
|
|
5
|
+
This patch preserves observed serving models in run history and reports for more reliable usage attribution and pricing. (#57)
|
|
6
|
+
|
|
7
|
+
## Runs and statistics
|
|
8
|
+
|
|
9
|
+
- Expose `usage_model` separately from the requested `model` in completed run JSON and registry reports, and retain observed model evidence in interactive history even when token usage is unavailable.
|
|
10
|
+
- Recover Codex model evidence from the exact rollout identified by `thread.started.thread_id`, while keeping token totals from the run's own output. Configured defaults alone do not establish the serving model; missing or mixed model evidence stays unpriced.
|
|
11
|
+
- Group Claude's `[1m]` context variants with their base model in `cdx stats --by model`, preserving the original model string and per-run pricing.
|
|
12
|
+
|
|
13
|
+
These changes apply to newly recorded runs; existing history is not rewritten.
|
|
@@ -595,6 +595,17 @@
|
|
|
595
595
|
"x86_64-pc-windows-msvc": "89a7e43654ea5f0d4efc20a14c1398377a1d48485f1b9867b199e85aa9c25daf",
|
|
596
596
|
"x86_64-unknown-linux-musl": "a36a3589a3766d67e44a0466b11a18bda4297963416eec7b5b9d23a5c9f363a4"
|
|
597
597
|
}
|
|
598
|
+
},
|
|
599
|
+
"v0.24.2": {
|
|
600
|
+
"github_tarball_sha256": "90ea3d050c278500b975bbc8bfe1b185f90fc35dfcf6b07d84eb59439bb24e58",
|
|
601
|
+
"github_zip_sha256": "3c53f0f0550416ebe3d7281e9c823a682a2c3560130df6fafec48d35278f4f8a",
|
|
602
|
+
"tray_assets": {
|
|
603
|
+
"aarch64-apple-darwin": "8fc48c8772b5ea50700933f0836e153104ef328b395de059749bd5a5aa864076",
|
|
604
|
+
"aarch64-unknown-linux-musl": "ce7708f4196cfdf7b7226ba3199e772e767c2d470c37d19589cb41bfc35de7af",
|
|
605
|
+
"x86_64-apple-darwin": "2d1d6e84ee08d9dea5a707332a39509edbe68b05e3d062bdae93e9c6704db543",
|
|
606
|
+
"x86_64-pc-windows-msvc": "eecc8f8c46d2ce50e6cf6ec6c48c10a10e165ec7ec0058cfb55c8aa486d17d7b",
|
|
607
|
+
"x86_64-unknown-linux-musl": "4cf3289c5d9d7a540a032a11d27e93093d69ec507e1796a726f429c0670f7bc5"
|
|
608
|
+
}
|
|
598
609
|
}
|
|
599
610
|
}
|
|
600
611
|
}
|
package/docs/cli.md
CHANGED
|
@@ -269,6 +269,8 @@ The figure uses standard public list prices, excluding Fast/priority, Batch/Flex
|
|
|
269
269
|
|
|
270
270
|
`cdx stats` counts completed history attempts. A headless failover has one logical run in `cdx runs` but one history row per attempted session, including failed attempts that reported usage. Period filtering includes the entire run in every period it overlaps, so adjacent daily results can both include a run crossing midnight; adding daily totals may exceed a combined period. JSON `accounting` names this policy and the current price-table source. `usage_runs` and `priced_runs` show how much of the token total has a dollar estimate. Live runs are absent until their history row is written, and unknown usage is different from measured zero.
|
|
271
271
|
|
|
272
|
+
Completed run JSON and registry reports expose `usage_model` separately from the requested `model`. Pricing uses the observed model from provider output; Codex can also supply it through the exact rollout identified by `thread.started.thread_id`. Configured defaults alone never establish serving-model identity, and missing or mixed model evidence stays unpriced. `--by model` groups Claude's `[1m]` context variant with its base model while preserving the original model string and per-run pricing. These changes apply to newly recorded runs; existing history is not rewritten.
|
|
273
|
+
|
|
272
274
|
The built-in price table ships with **cdx-manager**. Run `cdx update` to receive new model prices; updating Codex or Claude alone does not refresh it. Then check `cdx --version` and `cdx stats --json`. Existing runs with a recorded model are recalculated using the new table when stats are read. `CDX_TOKEN_PRICES` overrides still take precedence. Maintainers: follow [the model and pricing update runbook](https://github.com/AlexAgo83/cdx-manager/blob/main/logics/runbook/run_005_maintaining_the_token_price_table.md) for source checks, tests, release preparation and installed-version verification.
|
|
273
275
|
|
|
274
276
|
A model absent from the table stays unpriced rather than being charged a default tier, and the totals line names it — which is exactly the key `CDX_TOKEN_PRICES` wants:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/commands/launch.py
CHANGED
|
@@ -458,16 +458,16 @@ def _attach_interactive_usage(session, run_info, history=None, baseline=None, un
|
|
|
458
458
|
# way as an id match would let a wrong attribution read as a
|
|
459
459
|
# measurement, which is the failure this replaced.
|
|
460
460
|
run_info["provider_transcript_match"] = match
|
|
461
|
-
if not cumulative:
|
|
462
|
-
return run_info
|
|
463
|
-
|
|
464
|
-
run_info["usage_cumulative"] = cumulative
|
|
465
461
|
if model:
|
|
466
462
|
# Which model actually served the run, read from the transcript rather
|
|
467
463
|
# than inferred from the session's configured launch settings -- where
|
|
468
464
|
# `model` is optional and usually unset, because the session takes the
|
|
469
465
|
# provider's default.
|
|
470
466
|
run_info["usage_model"] = model
|
|
467
|
+
if not cumulative:
|
|
468
|
+
return run_info
|
|
469
|
+
|
|
470
|
+
run_info["usage_cumulative"] = cumulative
|
|
471
471
|
if uncertain_reason:
|
|
472
472
|
run_info["usage_attribution"] = uncertain_reason
|
|
473
473
|
return run_info
|
package/src/commands/runs.py
CHANGED
|
@@ -687,6 +687,7 @@ def handle_run(rest, ctx):
|
|
|
687
687
|
)
|
|
688
688
|
usage, usage_model = extract_run_observation(
|
|
689
689
|
run_session.get("provider"), run_info.get("stdout_path"),
|
|
690
|
+
run_session.get("authHome"),
|
|
690
691
|
)
|
|
691
692
|
run_info = {**run_info, "usage": usage, "usage_model": usage_model}
|
|
692
693
|
ctx["service"]["record_launch_history"](run_session["name"], {
|
|
@@ -807,6 +808,7 @@ def handle_run(rest, ctx):
|
|
|
807
808
|
if run_info and run_id:
|
|
808
809
|
usage, usage_model = extract_run_observation(
|
|
809
810
|
run_session.get("provider"), run_info.get("stdout_path"),
|
|
811
|
+
run_session.get("authHome"),
|
|
810
812
|
)
|
|
811
813
|
run_info = {**run_info, "usage": usage, "usage_model": usage_model}
|
|
812
814
|
ctx["service"]["record_launch_history"](run_session["name"], {
|
package/src/commands/status.py
CHANGED
|
@@ -491,7 +491,8 @@ def _stats_group(entry, by):
|
|
|
491
491
|
"""The project (launch folder) or the model a run is counted under."""
|
|
492
492
|
if by == "project":
|
|
493
493
|
return str(entry.get("cwd") or "").rstrip("/\\") or "-"
|
|
494
|
-
|
|
494
|
+
model = entry.get("usage_model") or "-"
|
|
495
|
+
return model.removesuffix("[1m]") if model.startswith("claude-") else model
|
|
495
496
|
|
|
496
497
|
|
|
497
498
|
def _project_labels(paths):
|
package/src/run_command.py
CHANGED
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
from .cli_args import ARG_CODE_INVALID_VALUE
|
|
5
5
|
from .errors import CdxArgumentError, CdxError
|
|
6
6
|
from .provider_runtime import headless_permission_disables_network
|
|
7
|
-
from .run_usage import empty_usage,
|
|
7
|
+
from .run_usage import empty_usage, extract_run_observation
|
|
8
8
|
|
|
9
9
|
PROMPT_STDIN = "-"
|
|
10
10
|
|
|
@@ -182,11 +182,11 @@ def run_launch_payload(api_schema_version, parsed, session, artifacts, cwd=None,
|
|
|
182
182
|
def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
183
183
|
error=None, error_source=None, error_code=None, selection_row=None):
|
|
184
184
|
run_info = run_info or {}
|
|
185
|
-
usage = run_info.get("usage")
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
usage, usage_model = run_info.get("usage"), run_info.get("usage_model")
|
|
186
|
+
if not isinstance(usage, dict):
|
|
187
|
+
usage, usage_model = extract_run_observation(
|
|
188
|
+
session.get("provider"), run_info.get("stdout_path"), session.get("authHome"),
|
|
189
|
+
) if session else (empty_usage(), None)
|
|
190
190
|
message = None if ok else (
|
|
191
191
|
_provider_failure_message(error, run_info, error_code)
|
|
192
192
|
if error_source == "provider"
|
|
@@ -221,6 +221,7 @@ def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
|
221
221
|
"stdout_path": run_info.get("stdout_path"),
|
|
222
222
|
"stderr_path": run_info.get("stderr_path"),
|
|
223
223
|
"usage": usage,
|
|
224
|
+
"usage_model": usage_model,
|
|
224
225
|
"warnings": warnings,
|
|
225
226
|
"error": None if ok else {
|
|
226
227
|
"source": error_source or "cdx",
|
package/src/run_registry.py
CHANGED
|
@@ -358,6 +358,7 @@ class RunRegistry:
|
|
|
358
358
|
# what `cdx run-status` and `cdx run-report` publish, and it
|
|
359
359
|
# used to store whichever shape the producing reader emitted.
|
|
360
360
|
run["usage"] = coerce_usage(final_payload.get("usage"))
|
|
361
|
+
run["usage_model"] = final_payload.get("usage_model")
|
|
361
362
|
run["final_payload"] = final_payload
|
|
362
363
|
if error:
|
|
363
364
|
run["error"] = error
|
package/src/run_usage.py
CHANGED
|
@@ -413,7 +413,7 @@ def extract_run_usage(provider, stdout_path):
|
|
|
413
413
|
return extract_run_observation(provider, stdout_path)[0]
|
|
414
414
|
|
|
415
415
|
|
|
416
|
-
def extract_run_observation(provider, stdout_path):
|
|
416
|
+
def extract_run_observation(provider, stdout_path, auth_home=None):
|
|
417
417
|
if not stdout_path or not provider:
|
|
418
418
|
return empty_usage(), None
|
|
419
419
|
if provider not in SUPPORTED_PROVIDERS:
|
|
@@ -444,9 +444,46 @@ def extract_run_observation(provider, stdout_path):
|
|
|
444
444
|
model_usage = record.get("modelUsage")
|
|
445
445
|
if isinstance(model_usage, dict):
|
|
446
446
|
models.update(model_usage)
|
|
447
|
+
if provider == "codex" and auth_home:
|
|
448
|
+
models.update(_codex_run_models(records, auth_home))
|
|
447
449
|
return usage, next(iter(models)) if len(models) == 1 else None
|
|
448
450
|
|
|
449
451
|
|
|
452
|
+
def _codex_run_models(records, auth_home):
|
|
453
|
+
# Exec's token totals belong to this run; a rollout's totals may include
|
|
454
|
+
# earlier turns. Read only model evidence from the exact thread's rollout.
|
|
455
|
+
from .interactive_usage import MAX_TRANSCRIPT_BYTES, conversation_transcript
|
|
456
|
+
|
|
457
|
+
thread_ids = {record.get("thread_id") for record in records
|
|
458
|
+
if isinstance(record, dict) and record.get("type") == "thread.started"
|
|
459
|
+
and isinstance(record.get("thread_id"), str) and record["thread_id"]}
|
|
460
|
+
if len(thread_ids) != 1:
|
|
461
|
+
return set()
|
|
462
|
+
path = conversation_transcript("codex", auth_home, next(iter(thread_ids)))
|
|
463
|
+
if not path:
|
|
464
|
+
return set()
|
|
465
|
+
models = set()
|
|
466
|
+
try:
|
|
467
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
468
|
+
text = handle.read(MAX_TRANSCRIPT_BYTES + 1)
|
|
469
|
+
if len(text) > MAX_TRANSCRIPT_BYTES:
|
|
470
|
+
return set()
|
|
471
|
+
for line in text.splitlines():
|
|
472
|
+
try:
|
|
473
|
+
record = json.loads(line)
|
|
474
|
+
except ValueError:
|
|
475
|
+
continue
|
|
476
|
+
if not isinstance(record, dict) or record.get("type") != "turn_context":
|
|
477
|
+
continue
|
|
478
|
+
payload = record.get("payload")
|
|
479
|
+
model = payload.get("model") if isinstance(payload, dict) else None
|
|
480
|
+
if isinstance(model, str) and model:
|
|
481
|
+
models.add(model)
|
|
482
|
+
except OSError:
|
|
483
|
+
return set()
|
|
484
|
+
return models
|
|
485
|
+
|
|
486
|
+
|
|
450
487
|
def _parse_json_records(text):
|
|
451
488
|
stripped = text.strip()
|
|
452
489
|
try:
|