Skip to content

core: say when a cached token count cannot be used - #58

Merged
VasiliyRad merged 1 commit into
mainfrom
codex/flag-unusable-token-counts
Sep 20, 2026
Merged

VasiliyRad merged 1 commit into
mainfrom
codex/flag-unusable-token-counts

Conversation

@VasiliyRad

Copy link
Copy Markdown
Collaborator

The gap

_tokens answers two different facts with the same None:

def _tokens(value):
    if value is None or isinstance(value, bool): return None
    try: result = int(value)
    except (ValueError, TypeError, OverflowError): return None
    return result if result >= 0 else None

"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:

cache_read_count = _tokens(cache_read_tokens) 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 was
billed at the full input rate and said nothing about it. A consumer that trusts
only a fully priced figure — the rule CostResult.status exists to support — had
no way to tell that from a real price.

Input and output counts were already safe: they carry no or 0 and already
report missing_*, which turns the result partial.

The change

Each cached field reports unusable_<field> when the value supplied cannot be
read:

unusable_cache_read_tokens
unusable_cache_write_tokens
unusable_cache_write_5m_tokens
unusable_cache_write_1h_tokens

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 longer
does. 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_pipeline had grown a
local 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 that
could not, because status never reflected the problem.

With this, the consumer drops its last piece of pricing detail.

Verification

$ PYTHONPATH=core/src pytest core/tests -q
264 passed, 1 failed

The single failure is test_package_release_gate, which shells out to
python -m build; the local venv has no build module. It fails identically
on 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 an
unusable 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

@VasiliyRad
VasiliyRad force-pushed the codex/flag-unusable-token-counts branch from 21c98b4 to cca9fef Compare September 20, 2026 16:53
`_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
VasiliyRad force-pushed the codex/flag-unusable-token-counts branch from cca9fef to 225be7d Compare September 20, 2026 16:57
@VasiliyRad
VasiliyRad merged commit 60b5dcd into main Sep 20, 2026
4 checks passed
@VasiliyRad
VasiliyRad deleted the codex/flag-unusable-token-counts branch September 20, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant