Skip to content

fix: stop quietly coercing the usage numbers - #10

Draft
simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/validate-usage-fields
Draft

simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:fix/validate-usage-fields

Conversation

@simonx1

@simonx1 simonx1 commented Sep 18, 2026

Copy link
Copy Markdown

usage read its fields through Integer(value, exception: false) and Float(value, exception: false), which is a parser, not a check. Verified:

wire value became
30.9 30
"120" 120
"120.5" nil
[], {}, true nil

It accepted things that are not token counts and produced a number anyway, and everything it couldn't read became nil — which is exactly what a provider that doesn't report the field produces. There was no way to tell "this provider sends no cost" from "this response was malformed".

Requests are billed per input token. A usage number that silently lost its fraction, or silently became nil, is worse than no number, because whatever is adding these up keeps adding them up.

The fix

A present field has to be a whole token count — an Integer, or a Float with nothing after the point, since JSON has one number type and 120 may arrive as 120.0. Anything else raises InvalidResponse naming the field and showing the value. An absent field is still nil, and a provider that doesn't report cost still reports nil without looking at what was sent.

Tests

test/usage_test.rb, 14 cases. One existing test that asserted junk becomes nil now asserts it's rejected.


Draft: part of a security and API-coverage audit, opened for reference rather than as a request for immediate review. Independent of the other branches, each off main. Suite green on Ruby 3.2.11, 3.3.8 and 3.4.8.

🤖 Generated with Claude Code

usage read its fields through Integer(value, exception: false) and
Float(value, exception: false), which is a parser, not a check. It
accepted things that are not token counts and produced a number anyway:
30.9 became 30, "120" became 120. And everything it could not read at all
-- [], true, "120.5", an error object sitting where usage should be --
became nil, which is exactly what a provider that does not report the
field produces. There was no way to tell "this provider sends no cost"
from "this response was malformed".

Requests are billed per input token. A usage number that silently lost
its fraction, or silently became nil, is worse than no number, because
whatever is adding these up keeps adding them up.

A present field now has to be a whole token count -- an Integer, or a
Float with nothing after the point, since JSON has one number type and
120 may arrive as 120.0. Anything else raises InvalidResponse naming the
field and showing the value. An absent field is still nil, and a provider
that does not report cost still reports nil for it without looking at
what was sent.

The test that asserted junk becomes nil now asserts it is rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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