cdx-manager 0.20.4 → 0.20.6
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 +13 -9
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_20_5.md +22 -0
- package/changelogs/CHANGELOGS_0_20_6.md +14 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/codex_usage.py +10 -1
- package/src/commands/auth.py +18 -16
- package/src/health.py +7 -2
- package/src/provider_runtime.py +15 -13
- package/src/run_usage.py +25 -15
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
|
|
10
10
|
|
|
@@ -725,13 +725,13 @@ Every usage record, whichever path produced it, uses one set of definitions:
|
|
|
725
725
|
|
|
726
726
|
Cached input is counted once: it is excluded from `input_tokens` and reported in the cache fields, never in both. `null` means the provider did not report the figure, which is not the same as zero.
|
|
727
727
|
|
|
728
|
-
`cdx stats` also shows a **`COST~`** column and ranks sessions by it. It weights each token class by what it costs relative to one uncached input token — cache read 0.1x, cache write 1.25x, output
|
|
728
|
+
`cdx stats` also shows a **`COST~`** column and ranks sessions by it. It weights each token class by what it costs relative to one uncached input token — cache read 0.1x, cache write 1.25x, and output at the recorded model's output/input ratio — and reports the result in uncached-input-equivalent tokens. These are ratios rather than prices. It is **not** a currency amount, and it is not comparable across providers whose own prices differ. The cache-write multiplier assumes the five-minute cache TTL; a one-hour TTL costs 2x and nothing in a transcript says which was used, so long-TTL writes are slightly under-weighted.
|
|
729
729
|
|
|
730
730
|
Ranking on the raw total would put a session that mostly replayed a cache above one that spent far more on generation, because cache reads are typically the overwhelming majority of tokens consumed while costing a fiftieth of an output token.
|
|
731
731
|
|
|
732
732
|
The **`USD~`** column turns that into money for runs whose serving model cdx recorded. It needs one number per model — the list price per million uncached input tokens — because `COST~` has already absorbed every other rate. Prices live in configuration, not in code: set `CDX_TOKEN_PRICES` to a JSON object mapping model id to dollars per million tokens to override the built-in table, whose last review date is printed with the totals. A run whose model cdx never saw stays unpriced rather than being charged at a default tier, and the totals line says how many runs were actually priced.
|
|
733
733
|
|
|
734
|
-
The table carries two numbers per model — uncached input and output per million tokens — and derives the cache rates from the input rate: a cache read is 0.1x it and a cache write 1.25x, a relationship that holds on
|
|
734
|
+
The table carries two numbers per model — uncached input and output per million tokens — and derives the cache rates from the input rate: a cache read is 0.1x it and a cache write 1.25x, a relationship that holds on the supported non-Pro model rows. Each token class is then priced at its own rate rather than through a weighted shortcut, because output/input ratios vary by model.
|
|
735
735
|
|
|
736
736
|
Three caveats stated rather than hidden. A run is priced at the model serving its most recent record, so a run that switched models mid-way is priced at the newer one. The figure is an estimate at public list prices — not an invoice. And OpenAI's long-context tier, which roughly doubles both rates above a 272K-token request, is **not** modelled: cdx records tokens per run rather than per request, so it cannot tell which requests crossed that line, and long-context Codex work is under-costed.
|
|
737
737
|
|
|
@@ -742,7 +742,7 @@ Totals: 26 runs, ... ; no price for gpt-6-unreleased)
|
|
|
742
742
|
CDX_TOKEN_PRICES='{"gpt-6-unreleased": {"input": 3, "output": 18}}' cdx stats
|
|
743
743
|
```
|
|
744
744
|
|
|
745
|
-
**Prices are the only part of this that rots on its own.** A test fails once the table has gone 90 days without review, and `scripts/check_token_prices.py` compares it against published pricing — outside the test suite, since a check that needs the network fails for reasons nobody wants in CI. A row it cannot confirm is reported as unverified, never as passing. When it finds a difference, follow [`
|
|
745
|
+
**Prices are the only part of this that rots on its own.** A test fails once the table has gone 90 days without review, and `scripts/check_token_prices.py` compares it against published pricing — outside the test suite, since a check that needs the network fails for reasons nobody wants in CI. A row it cannot confirm is reported as unverified, never as passing. When it finds a difference, follow [`logics/runbook/run_005_maintaining_the_token_price_table.md`](logics/runbook/run_005_maintaining_the_token_price_table.md) — which also records where each vendor's numbers actually live.
|
|
746
746
|
|
|
747
747
|
Runs recorded before these definitions existed carry figures that are fictitious rather than imprecise — one real session reported 206.6M cached tokens for a period in which its own conversation consumed 33,608, because the old reader picked whichever transcript had the newest modification time and re-read all of it on every launch. They cannot be recomputed: a run's true usage is the increment its transcript gained while it ran, and nothing recorded that. They are therefore excluded from every total automatically, and the totals line says how many runs that covered — so a period containing only such runs reads as absent rather than as a session that spent nothing. The records themselves are left alone, which keeps them recoverable if the marker ever proves wrong; `cdx repair` clears them from the file for anyone who would rather they were gone:
|
|
748
748
|
|
|
@@ -1092,11 +1092,15 @@ For example, a user-owned daily cron entry can run:
|
|
|
1092
1092
|
```
|
|
1093
1093
|
|
|
1094
1094
|
`locked` means an interactive session owns the profile refresh lock and was left
|
|
1095
|
-
untouched.
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1095
|
+
untouched. If every selected profile is locked, the command is non-zero because
|
|
1096
|
+
it verified no credential; a mix of valid and locked profiles remains successful.
|
|
1097
|
+
Bulk refresh skips disabled profiles, while `cdx auth refresh <name>` still lets
|
|
1098
|
+
an operator inspect one explicitly. `login_required` means the provider rejected
|
|
1099
|
+
the local refresh token; run the supplied `cdx login <name>` command interactively.
|
|
1100
|
+
Hosts keep independent rotating credentials, so copying an auth file is not ongoing
|
|
1101
|
+
credential sync.
|
|
1102
|
+
|
|
1103
|
+
- **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first. `cdx doctor` uses the same Codex app-server probe as `cdx auth refresh`, so a local auth file alone is never reported as live authentication; a rejected refresh token is reported as `login_required` with the matching login command.
|
|
1100
1104
|
- **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file`, `codex_live_auth`, and `codex_stale_auth_logs` checks. If recent logs mention expired auth, repair only that isolated profile with `cdx login <name>`. Codex Business profiles can share the same `tokens.account_id`; `cdx doctor` reports `codex_shared_account_id` as a workspace-level clue, not proof that two profiles use the same user. Newly created sessions seed from the current global `~/.codex/auth.json` when one exists. For two separate Codex accounts, create or repair each session by running `cdx login <name>` for that session; `cdx login` does not log out first, so use `cdx logout <name>` explicitly only when you want to clear that isolated profile.
|
|
1101
1105
|
- **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
|
|
1102
1106
|
- **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.20.
|
|
1
|
+
0.20.6
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# CDX Manager 0.20.5
|
|
2
|
+
|
|
3
|
+
## Fixes
|
|
4
|
+
|
|
5
|
+
- `cdx doctor` now uses the live Codex app-server authentication probe instead
|
|
6
|
+
of trusting a local auth file, so an expired or rejected refresh token is
|
|
7
|
+
reported as `login_required` with an actionable `cdx login <name>` remedy.
|
|
8
|
+
- `cdx auth refresh all` skips disabled Codex profiles, while an explicit
|
|
9
|
+
`cdx auth refresh <name>` still lets an operator inspect a disabled profile
|
|
10
|
+
deliberately.
|
|
11
|
+
- A bulk Codex auth refresh where every selected profile is locked now exits
|
|
12
|
+
non-zero as `auth_refresh_indeterminate` instead of looking like a silent
|
|
13
|
+
success without verifying any credential.
|
|
14
|
+
- Codex app-server diagnostics no longer keep an unread stderr pipe while
|
|
15
|
+
waiting for stdout, avoiding a possible deadlock or timeout from noisy
|
|
16
|
+
provider diagnostics.
|
|
17
|
+
|
|
18
|
+
## Validation
|
|
19
|
+
|
|
20
|
+
- Package smoke coverage now runs `cdx doctor --json` from both packed npm and
|
|
21
|
+
wheel installs with isolated local provider shims, so packaged doctor
|
|
22
|
+
regressions are caught outside the source checkout.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CDX Manager 0.20.6
|
|
2
|
+
|
|
3
|
+
## Fixes
|
|
4
|
+
|
|
5
|
+
- `cdx stats` now prices current Codex/OpenAI and Claude models that were
|
|
6
|
+
previously unpriced or stale, including `gpt-5.5`, `gpt-5.6-terra`, and the
|
|
7
|
+
current Claude Sonnet/Haiku snapshot ids seen in real usage history.
|
|
8
|
+
- The token price review tooling now checks OpenAI's official pricing
|
|
9
|
+
documentation directly and points maintainers at the active Logics runbook.
|
|
10
|
+
|
|
11
|
+
## Notes
|
|
12
|
+
|
|
13
|
+
- Pro-only or credit-based Codex models without an official token price remain
|
|
14
|
+
unpriced rather than receiving a guessed USD estimate.
|
|
@@ -353,6 +353,28 @@
|
|
|
353
353
|
"x86_64-pc-windows-msvc": "1f9971c1aee7e92685370db1e6a4bcf37b6012fa7452fc14ca3dd545998af82e",
|
|
354
354
|
"x86_64-unknown-linux-musl": "2aabbc00b0fa3a2c8862cc1a5425a074fec99f072bf7b695d31e6e96b9b08c5d"
|
|
355
355
|
}
|
|
356
|
+
},
|
|
357
|
+
"v0.20.4": {
|
|
358
|
+
"github_tarball_sha256": "55323fe428d6d297f14a210a590757443080dc55a87bb6fae4d967fddb505af3",
|
|
359
|
+
"github_zip_sha256": "37f4abb392b372d0f5fe5d5eb7c46e1e81a30b319d1961b5518f20e776f59b75",
|
|
360
|
+
"tray_assets": {
|
|
361
|
+
"aarch64-apple-darwin": "6d61a45ab6f310a83aefbb80d77dc31b1f3a738640197bcca1264c6ca7b70d26",
|
|
362
|
+
"aarch64-unknown-linux-musl": "f2f3a2c45c7deb2ea69cdc7c7b68524e6b5db5d1eda0ff902bda49d875636402",
|
|
363
|
+
"x86_64-apple-darwin": "93d3d9180588edcf573ffa8f9f2e870e8dff9659e198cde531bec0acfc37184f",
|
|
364
|
+
"x86_64-pc-windows-msvc": "7508e04ee07adb7a500b346be1d3891d93aec54dc0ff53643bd8c57e4e411118",
|
|
365
|
+
"x86_64-unknown-linux-musl": "47a1c8cff60c896db659d30d74496045d8c343e74a00f826e608f3973e735532"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
"v0.20.5": {
|
|
369
|
+
"github_tarball_sha256": "6360b48c4e7c3e79f213174d55fa6679ea2e94b016e0de3ebe77a39cdac98856",
|
|
370
|
+
"github_zip_sha256": "6fed447ed9facf353205745265935fd5c7d73b877e7b933cc53b08656fb1b32a",
|
|
371
|
+
"tray_assets": {
|
|
372
|
+
"aarch64-apple-darwin": "5f2bf81fc17f49479e2880d0312e8f60bcd23a87b7d4978c7161015f75783f03",
|
|
373
|
+
"aarch64-unknown-linux-musl": "76a5a362f5a2c41cab23165760f7d9d3fb5bfbe5a2e9d5b9643afb30e6d65e0d",
|
|
374
|
+
"x86_64-apple-darwin": "ecc05376dd93c043b36dad6e9a27e129c00898227f204e0c8f050ee834138239",
|
|
375
|
+
"x86_64-pc-windows-msvc": "208064f2bfee19914ca72edb527966a9966154179d6d040f0dae4a647a61f9f4",
|
|
376
|
+
"x86_64-unknown-linux-musl": "9fa01d3b71daed98c9234ddb0cf7b6345b3d757edff910ece4e3bd9f5eb8eb6b"
|
|
377
|
+
}
|
|
356
378
|
}
|
|
357
379
|
}
|
|
358
380
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/codex_usage.py
CHANGED
|
@@ -199,6 +199,15 @@ def fetch_codex_rate_limit_diagnostic(session, timeout=5, popen_factory=None):
|
|
|
199
199
|
return _probe_codex_rate_limit_diagnostic(session, auth_home, timeout, popen_factory)
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
def diagnostic_needs_codex_login(diagnostic):
|
|
203
|
+
"""Classify known auth failures without returning provider text."""
|
|
204
|
+
text = json.dumps(diagnostic.get("response") or {}).lower()
|
|
205
|
+
return any(marker in text for marker in (
|
|
206
|
+
"not logged in", "unauthorized", "authentication token is expired",
|
|
207
|
+
"token_expired", "invalid_grant", "http 401",
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
|
|
202
211
|
def _probe_codex_rate_limit_diagnostic(session, auth_home, timeout=5, popen_factory=None):
|
|
203
212
|
diagnostic = _request_codex_app_server(
|
|
204
213
|
auth_home,
|
|
@@ -236,7 +245,7 @@ def _request_codex_app_server(auth_home, method, params, failure_reason, timeout
|
|
|
236
245
|
["codex", "app-server", "--listen", "stdio://"],
|
|
237
246
|
stdin=subprocess.PIPE,
|
|
238
247
|
stdout=subprocess.PIPE,
|
|
239
|
-
stderr=subprocess.
|
|
248
|
+
stderr=subprocess.DEVNULL,
|
|
240
249
|
env=env,
|
|
241
250
|
text=True,
|
|
242
251
|
bufsize=1,
|
package/src/commands/auth.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import json
|
|
7
6
|
import time
|
|
8
7
|
import uuid
|
|
9
8
|
|
|
@@ -18,7 +17,11 @@ from ..cli_helpers import (
|
|
|
18
17
|
_write_json,
|
|
19
18
|
)
|
|
20
19
|
from ..cli_render import _success, _warn
|
|
21
|
-
from ..codex_usage import
|
|
20
|
+
from ..codex_usage import (
|
|
21
|
+
consume_codex_rate_limit_reset_credit,
|
|
22
|
+
diagnostic_needs_codex_login,
|
|
23
|
+
fetch_codex_rate_limit_diagnostic,
|
|
24
|
+
)
|
|
22
25
|
from ..config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
23
26
|
from ..errors import CdxError
|
|
24
27
|
from ..notify import (
|
|
@@ -36,21 +39,12 @@ def _confirm_reset(name):
|
|
|
36
39
|
return answer.strip().lower() in ("y", "yes")
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
def _refresh_needs_login(diagnostic):
|
|
40
|
-
"""Classify known provider auth failures without returning provider text."""
|
|
41
|
-
text = json.dumps(diagnostic.get("response") or {}).lower()
|
|
42
|
-
return any(marker in text for marker in (
|
|
43
|
-
"not logged in", "unauthorized", "authentication token is expired",
|
|
44
|
-
"token_expired", "invalid_grant", "http 401",
|
|
45
|
-
))
|
|
46
|
-
|
|
47
|
-
|
|
48
42
|
def _refresh_result(session, diagnostic):
|
|
49
43
|
if diagnostic.get("ok"):
|
|
50
44
|
return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "valid"}
|
|
51
45
|
if diagnostic.get("reason") == "auth_locked":
|
|
52
46
|
return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "locked"}
|
|
53
|
-
if
|
|
47
|
+
if diagnostic_needs_codex_login(diagnostic):
|
|
54
48
|
return {
|
|
55
49
|
"session": session["name"], "provider": PROVIDER_CODEX,
|
|
56
50
|
"outcome": "login_required", "login_command": f"cdx login {session['name']}",
|
|
@@ -63,7 +57,7 @@ def handle_auth(rest, ctx):
|
|
|
63
57
|
target = parsed["target"]
|
|
64
58
|
if target == "all":
|
|
65
59
|
sessions = [session for session in ctx["service"]["list_sessions"]()
|
|
66
|
-
if session["provider"] == PROVIDER_CODEX]
|
|
60
|
+
if session["provider"] == PROVIDER_CODEX and session.get("enabled", True)]
|
|
67
61
|
else:
|
|
68
62
|
session = ctx["service"]["get_session"](target)
|
|
69
63
|
if not session:
|
|
@@ -71,24 +65,32 @@ def handle_auth(rest, ctx):
|
|
|
71
65
|
if session["provider"] != PROVIDER_CODEX:
|
|
72
66
|
raise CdxError("Authentication refresh is only available for Codex sessions.")
|
|
73
67
|
sessions = [session]
|
|
74
|
-
if not sessions:
|
|
68
|
+
if not sessions and target != "all":
|
|
75
69
|
raise CdxError("No managed Codex sessions to refresh.")
|
|
76
70
|
|
|
77
71
|
probe = ctx["options"].get("fetchCodexRateLimitDiagnostic") or fetch_codex_rate_limit_diagnostic
|
|
78
72
|
results = [_refresh_result(session, probe(session)) for session in sessions]
|
|
79
73
|
failures = [result for result in results if result["outcome"] in ("login_required", "failed")]
|
|
80
|
-
|
|
74
|
+
all_locked = bool(results) and all(result["outcome"] == "locked" for result in results)
|
|
75
|
+
message = (
|
|
76
|
+
"Codex authentication probe needs attention" if failures
|
|
77
|
+
else "Codex authentication probe could not verify any session" if all_locked
|
|
78
|
+
else "No enabled Codex sessions to refresh" if not results
|
|
79
|
+
else "Codex authentication probe completed"
|
|
80
|
+
)
|
|
81
81
|
if parsed["json"]:
|
|
82
82
|
payload = (
|
|
83
83
|
_json_failure("auth.refresh", "auth_refresh_failed", message, results=results)
|
|
84
84
|
if failures else _json_success("auth.refresh", message, results=results)
|
|
85
85
|
)
|
|
86
|
+
if all_locked:
|
|
87
|
+
payload = _json_failure("auth.refresh", "auth_refresh_indeterminate", message, results=results)
|
|
86
88
|
_write_json(ctx, payload)
|
|
87
89
|
else:
|
|
88
90
|
for result in results:
|
|
89
91
|
suffix = f"; run {result['login_command']}" if result.get("login_command") else ""
|
|
90
92
|
ctx["out"](f"{result['session']}: {result['outcome']}{suffix}\n")
|
|
91
|
-
return 1 if failures else 0
|
|
93
|
+
return 1 if failures or all_locked else 0
|
|
92
94
|
|
|
93
95
|
def handle_ready(rest, ctx):
|
|
94
96
|
"""`cdx ready`: schedule an OS notification for the next session to come back.
|
package/src/health.py
CHANGED
|
@@ -337,11 +337,16 @@ def _codex_auth_issues(session, diag):
|
|
|
337
337
|
]
|
|
338
338
|
live_status = diag.get("live_status")
|
|
339
339
|
live_ok = live_status == "authenticated"
|
|
340
|
-
live_warn = live_status in ("logged_out", "error")
|
|
340
|
+
live_warn = live_status in ("logged_out", "login_required", "error")
|
|
341
|
+
live_message = (
|
|
342
|
+
f"session {name} Codex authentication requires login; run cdx login {name}"
|
|
343
|
+
if live_status == "login_required"
|
|
344
|
+
else f"session {name} live Codex auth status: {live_status}"
|
|
345
|
+
)
|
|
341
346
|
issues.append(_issue(
|
|
342
347
|
"OK" if live_ok else "WARN" if live_warn else "WARN",
|
|
343
348
|
"codex_live_auth",
|
|
344
|
-
|
|
349
|
+
live_message,
|
|
345
350
|
{
|
|
346
351
|
"session": name,
|
|
347
352
|
"auth_home": diag.get("auth_home"),
|
package/src/provider_runtime.py
CHANGED
|
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
|
|
|
13
13
|
|
|
14
14
|
from .agent_notify import launch_notify_env, notifications_enabled
|
|
15
15
|
from .claude_usage import _clean_oauth_token, _decode_jwt_claims
|
|
16
|
-
from .codex_usage import codex_auth_lock
|
|
16
|
+
from .codex_usage import codex_auth_lock, diagnostic_needs_codex_login, fetch_codex_rate_limit_diagnostic
|
|
17
17
|
from .config import (
|
|
18
18
|
PROVIDER_ANTIGRAVITY,
|
|
19
19
|
PROVIDER_CLAUDE,
|
|
@@ -294,19 +294,21 @@ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
|
|
|
294
294
|
"live_status": "unknown",
|
|
295
295
|
"live_error": None,
|
|
296
296
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
result["live_status"] =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
297
|
+
if not result["local_tokens_present"]:
|
|
298
|
+
result["live_status"] = AUTH_PROBE_LOGGED_OUT
|
|
299
|
+
return result
|
|
300
|
+
diagnostic = fetch_codex_rate_limit_diagnostic(session)
|
|
301
|
+
if diagnostic["ok"]:
|
|
302
|
+
result["live_status"] = AUTH_PROBE_AUTHENTICATED
|
|
303
|
+
elif diagnostic["reason"] == "auth_locked":
|
|
304
|
+
result["live_status"] = AUTH_PROBE_DEGRADED
|
|
305
|
+
result["live_error"] = "Codex authentication probe is locked by an active session."
|
|
306
|
+
elif diagnostic_needs_codex_login(diagnostic):
|
|
307
|
+
result["live_status"] = "login_required"
|
|
308
|
+
result["live_error"] = f"Codex authentication requires login; run cdx login {session['name']}."
|
|
309
|
+
else:
|
|
308
310
|
result["live_status"] = "error"
|
|
309
|
-
result["live_error"] =
|
|
311
|
+
result["live_error"] = f"Codex app-server authentication probe failed ({diagnostic['reason']})."
|
|
310
312
|
return result
|
|
311
313
|
|
|
312
314
|
|
package/src/run_usage.py
CHANGED
|
@@ -99,9 +99,8 @@ CACHE_READ_MULTIPLIER = 0.1
|
|
|
99
99
|
CACHE_WRITE_MULTIPLIER = 1.25
|
|
100
100
|
|
|
101
101
|
#: What an output token costs relative to an uncached input one, when the model
|
|
102
|
-
#: is unknown.
|
|
103
|
-
#:
|
|
104
|
-
#: direction.
|
|
102
|
+
#: is unknown. Current model ratios vary, so an unknown model is priced at the
|
|
103
|
+
#: low known ratio: understating a cost cdx is guessing at is the safer direction.
|
|
105
104
|
DEFAULT_OUTPUT_MULTIPLIER = 5.0
|
|
106
105
|
|
|
107
106
|
|
|
@@ -155,13 +154,11 @@ def weighted_usage(usage, model=None, prices=None):
|
|
|
155
154
|
#: rather than editing code, and treat an unknown model as unpriced instead of
|
|
156
155
|
#: assuming a tier. `scripts/check_token_prices.py` re-checks this table
|
|
157
156
|
#: against published pricing and a staleness test fails when nobody has;
|
|
158
|
-
#: `
|
|
157
|
+
#: `logics/runbook/run_005_maintaining_the_token_price_table.md` records where the numbers live and which
|
|
159
158
|
#: vendor pages refuse to be read.
|
|
160
159
|
#:
|
|
161
|
-
#: Anthropic figures come from the
|
|
162
|
-
#:
|
|
163
|
-
#: automated fetches; four sources agreed on $2.50/$15 for Terra and one
|
|
164
|
-
#: outlier said $2/$12, so these carry more doubt than the Anthropic rows.
|
|
160
|
+
#: Anthropic and OpenAI figures come from the vendors' own references, checked
|
|
161
|
+
#: on the review date below.
|
|
165
162
|
#:
|
|
166
163
|
#: Not modelled: OpenAI's long-context tier, which roughly doubles both rates
|
|
167
164
|
#: above a 272K-token request. cdx records tokens per run, not per request, so
|
|
@@ -175,15 +172,29 @@ DEFAULT_TOKEN_PRICES = {
|
|
|
175
172
|
"claude-opus-4-8": {"input": 5.0, "output": 25.0},
|
|
176
173
|
"claude-opus-4-7": {"input": 5.0, "output": 25.0},
|
|
177
174
|
"claude-opus-4-6": {"input": 5.0, "output": 25.0},
|
|
178
|
-
"claude-
|
|
175
|
+
"claude-opus-4-5": {"input": 5.0, "output": 25.0},
|
|
176
|
+
"claude-sonnet-5": {"input": 2.0, "output": 10.0},
|
|
179
177
|
"claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
|
|
178
|
+
"claude-sonnet-4-5": {"input": 3.0, "output": 15.0},
|
|
179
|
+
"claude-sonnet-4-5-20250929": {"input": 3.0, "output": 15.0},
|
|
180
180
|
"claude-haiku-4-5": {"input": 1.0, "output": 5.0},
|
|
181
|
+
"claude-haiku-4-5-20251001": {"input": 1.0, "output": 5.0},
|
|
181
182
|
# OpenAI
|
|
182
|
-
"gpt-5.6
|
|
183
|
-
"gpt-5.6-
|
|
184
|
-
"gpt-5.6-
|
|
183
|
+
"gpt-5.6": {"input": 4.0, "output": 20.0},
|
|
184
|
+
"gpt-5.6-sol": {"input": 4.0, "output": 20.0},
|
|
185
|
+
"gpt-5.6-terra": {"input": 2.0, "output": 12.0},
|
|
186
|
+
"gpt-5.6-luna": {"input": 0.2, "output": 1.2},
|
|
187
|
+
"gpt-5.5": {"input": 5.0, "output": 30.0},
|
|
188
|
+
"gpt-5.5-2026-04-23": {"input": 5.0, "output": 30.0},
|
|
189
|
+
"gpt-5.4": {"input": 2.5, "output": 15.0},
|
|
190
|
+
"gpt-5.4-2026-03-05": {"input": 2.5, "output": 15.0},
|
|
191
|
+
"gpt-5.4-mini": {"input": 0.75, "output": 4.5},
|
|
192
|
+
"gpt-5.4-mini-2026-03-17": {"input": 0.75, "output": 4.5},
|
|
193
|
+
"gpt-5.4-nano": {"input": 0.2, "output": 1.25},
|
|
194
|
+
"gpt-5.4-nano-2026-03-17": {"input": 0.2, "output": 1.25},
|
|
195
|
+
"gpt-5.3-codex": {"input": 1.75, "output": 14.0},
|
|
185
196
|
}
|
|
186
|
-
TOKEN_PRICES_REVIEWED = "2026-08-
|
|
197
|
+
TOKEN_PRICES_REVIEWED = "2026-08-22"
|
|
187
198
|
#: How long a review stays fresh before the staleness test asks for another.
|
|
188
199
|
TOKEN_PRICES_MAX_AGE_DAYS = 90
|
|
189
200
|
TOKEN_PRICES_ENV = "CDX_TOKEN_PRICES"
|
|
@@ -215,8 +226,7 @@ def estimate_cost(usage, model, prices=None):
|
|
|
215
226
|
|
|
216
227
|
Each token class is priced directly rather than through the weighted
|
|
217
228
|
figure. That shortcut was only correct while every model shared one
|
|
218
|
-
output-to-input ratio, and
|
|
219
|
-
it under-costed Codex output by a fifth.
|
|
229
|
+
output-to-input ratio, and they do not.
|
|
220
230
|
|
|
221
231
|
ponytail: a run is priced at one model -- the one serving its most recent
|
|
222
232
|
record -- rather than split per model. A session that switches models
|