Conversation
anomalyco#43248 guarded the cost operands but not the tier selection just above them, which still reads `item.tier.type` and `item.tier.size` unguarded. `cost()` in provider/provider.ts copies `tier: item.tier` straight from the models.dev / plugin payload with no guard of its own, so three shapes still abort the turn: a `tiers` entry missing its `tier`, a null entry, and a non-array `tiers`. Drop malformed tiers instead of trusting them. A non-numeric `size` is excluded rather than coerced to 0, since 0 would make the tier match every context instead of none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
— reviewer-h (automated AI reviewer) |
Two follow-ups from review on the first commit:
- a `tier.size` of 0 or a negative number passed `Number.isFinite` and, through
`contextTokens > size`, matched every context — so a corrupt tier won the
selection and billed at its own rates (verified: 108.9 instead of the base
4.5). Non-positive sizes are now dropped.
- the guard also regressed numeric strings. The comparison it replaced coerced
`contextTokens > "5000"`, and a hand-written config can produce one, so
`Number.isFinite("5000")` silently disabled a tier that used to apply.
`size` goes through `Number()` again, and only unparseable or non-positive
values are dropped.
Still deliberately not coerced to 0 on failure: that would match every context
instead of none, which is the bug above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — went through all four against the code. Two landed, two didn't. Pushed the fixes. 3 (negative Also fixed, which nobody flagged: my own guard regressed numeric strings. The comparison it replaced coerced 1 (tier without pricing) — the mechanism doesn't hold, but there is a real issue underneath. No NaN: 2 (silent drop) — agreed in principle, not here. 4 (b) — already covered. New tests cover the numeric string and both non-positive sizes. |
…zero-cost A cost tier with a valid shape and size but no finite input/output pricing (missing, undefined, or a numeric string) used to win selection over the base cost and silently zero the turn cost instead of falling back to the base rates. Treat such a tier as malformed and drop it, consistent with the existing guards for non-array tiers, null entries, missing `tier`, and non-positive sizes. Adds a getUsage test for a surviving pricing-less tier (now falls back to base).
|
Correction to my previous comment: point 1 did land here after all, in So of the four: 1 and 3 fixed, 2 still belongs at the Current numbers, all reproducible against this branch: base rates 4.5 for the test usage; |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Issue for this PR
Closes #43254
Type of change
What does this PR do?
#43248 guarded the operands that go into decimal.js, but not the tier selection two lines above them, which still reads
item.tier.typeanditem.tier.sizeunguarded.cost()inprovider/provider.tscopiestier: item.tierstraight from the models.dev / plugin payload with no guard of its own — unlikeinput/output/cache— so a malformedtiersstill throws out ofgetUsageand takes the turn with it.Three commits, each closing one way a malformed
cost.tiersmisbehaves:tiersthat is not an array, a null entry, or an entry missingtiereach threw.size: 0or a negative number passed the finite check and, throughcontextTokens > size, matched every context — a corrupt tier won the selection and billed at its own rates (108.9 instead of the base 4.5 in the test). Also fixes a regression the first commit introduced: the old comparison coercedcontextTokens > "5000", so requiring anumbersilently disabled tiers whosesizeis a numeric string.sizegoes throughNumber()again; only unparseable or non-positive values are dropped.tierbut no finiteinput/outputwon the selection and, via?? 0, producedcost = 0instead of falling back to the base rates.Throughout, a malformed value is dropped, never coerced to 0 — coercing is what makes a bad tier match every context instead of none.
How did you verify your code works?
Each case was reproduced first and confirmed failing on
devbefore writing the fix:cost.tiersdev[{ input, output, cache }](notier)TypeError: undefined is not an object[null]TypeError: null is not an object{}(not an array)TypeError: ...tiers?.filter is not a function[{ …, tier: { type: "context", size: -1 } }][{ tier: { type: "context", size: 5000 } }](no rates)Five tests in the existing
describe("SessionNs.getUsage")block cover them, plus one asserting a numeric-stringsizestill applies its tier. To confirm: revertsession.tsand re-run that file.bun test test/session/compaction.test.ts→ 61 pass, 1 skip, 0 fail.test/session/llm.test.ts+test/server/negative-tokens-regression.test.ts→ 29 pass.tsgo --noEmitclean, prettier and oxlint clean.Screenshots / recordings
Not a UI change.
Checklist