core: say when a cached token count cannot be used - #58
Merged
Merged
Conversation
VasiliyRad
force-pushed
the
codex/flag-unusable-token-counts
branch
from
September 20, 2026 16:53
21c98b4 to
cca9fef
Compare
`_tokens` answers "nothing was given" and "something was given that cannot be read" with the same `None`. That is right for a count a caller may legitimately omit and wrong for one it supplied, because the two have opposite billing consequences for cached tokens: absent means there was no cache to discount, unusable means we do not know. The cached counts then take `or 0`, so a supplied-but-unreadable value became zero with no reason recorded and the result stayed `priced`. A row whose input is almost entirely cache reads was billed at the full input rate and said nothing about it, and a caller that trusts only a fully priced figure had no way to tell that from a price. Input and output counts were already safe: they carry no `or 0` and already report `missing_*`, which turns the result `partial`. Each cached field now reports `unusable_<field>` when the value supplied cannot be read. The arithmetic is unchanged -- an unusable count still bills as nothing cached, because a guess would be worse than a flagged zero -- but the reason turns the result `partial`, which is the signal consumers already act on. An absent five-minute split still falls back to `cache_write_tokens`. A stated split that cannot be read does not: substituting a different field would bill a number the caller never gave. Numeric strings and floats are still accepted, as before; only values `int()` cannot take, negatives, and bools are unusable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VasiliyRad
force-pushed
the
codex/flag-unusable-token-counts
branch
from
September 20, 2026 16:57
cca9fef to
225be7d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
_tokensanswers two different facts with the sameNone:"Nothing was given" and "something was given that cannot be read" are not the
same thing, and for cached tokens they have opposite billing consequences:
absent means there was no cache to discount, unusable means we do not know.
The cached counts then take
or 0:So a supplied-but-unreadable value became zero, no reason was recorded, and the
result stayed
priced. A row whose input is almost entirely cache reads wasbilled at the full input rate and said nothing about it. A consumer that trusts
only a fully priced figure — the rule
CostResult.statusexists to support — hadno way to tell that from a real price.
Input and output counts were already safe: they carry no
or 0and alreadyreport
missing_*, which turns the resultpartial.The change
Each cached field reports
unusable_<field>when the value supplied cannot beread:
The arithmetic is unchanged. An unusable count still bills as nothing cached,
because a guess would be worse than a flagged zero. What is new is that the
reason turns the result
partial, which is the signal consumers already act on.One behaviour fix alongside it: an absent five-minute split still falls back
to
cache_write_tokens, but a stated split that cannot be read no longerdoes. Substituting a different field would bill a number the caller never gave.
Numeric strings and floats are still accepted exactly as before; only values
int()cannot take, negatives, and bools are unusable.Why this belongs here
It came out of a consumer review.
cost_opportunities_pipelinehad grown alocal guard rejecting malformed cache counts, which meant a pricing-library
consumer was carrying pricing knowledge to compensate for something the library
could not tell it. Everything else it had accumulated collapsed into
status != "priced"once that was pointed out — this was the one piece thatcould not, because
statusnever reflected the problem.With this, the consumer drops its last piece of pricing detail.
Verification
The single failure is
test_package_release_gate, which shells out topython -m build; the local venv has nobuildmodule. It fails identicallyon unmodified HEAD — confirmed by stashing the change and re-running. CI
installs
build, so it passes there.Five new tests: four unusable shapes are reported, an omitted count stays silent
and
priced, an unusable split does not fall back to the aggregate, and anunusable count costs exactly the same as an absent one. All five were confirmed
to fail without the change.
Version bumped 0.2.27 -> 0.2.28, matching how #52 and #55 shipped.
🤖 Generated with Claude Code