feat(usage): report per-model plan limits in their own bar row - #46
Merged
Merged
Conversation
Fable usage is reported. Per-model windows arrive in rate_limits.model_scoped, an array alongside the keyed windows, which parseUsageReport walked past entirely. Reading it is necessary and not sufficient: the binary does not relay that key, it synthesises it behind its own remote config (IUt(limits, jJe()) returns an empty list when tengu_usage_overage_included_models is empty, and the key is spliced in only when the projection yielded something), so in a --print session it never arrived. The raw rate_limits.limits[] array it projects from does arrive untouched, so the weekly_scoped entries that name a model are read from there too, with the binary's filter and without its allowlist -- that list selects overage billing, not which limits meter you. resets_at is epoch seconds there as often as a string and is normalised rather than deserialized. A refresh is now merged into the last one instead of replacing it. loadPlanRateLimits gives the usage endpoint 5s and falls back to seedUtilization(), an object rebuilt from the rate-limit response headers that can only carry five_hour and seven_day; it is flagged "seeded" and accepted identically downstream, so a failed poll was indistinguishable from one saying the per-model window is gone, and the bar blinked out and back. Merged by key over the whole set, since the opus and sonnet windows are missing from a seeded object for the same reason. The extra-credit balance is deliberately not carried. The plan limits moved out of the readout into their own responsive row under the status line, one labelled bar per window. Inline they trailed a wrapping row of unrelated metrics, so the windows nearest their cap were the first to wrap out of sight. Also in this cycle: an "Other models" group in the picker holding previous generations, with set_model sent as a correlated control request so a refusal restores the previous model instead of leaving the tab pointed at one every turn would fail on; the nimbus_quill window hidden on both ingestion paths rather than only the report; and quota notifications titled through UsageWindow.title(key) so a per-model window announces "Fable", not its synthetic key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What
rate_limits.model_scoped— an array the parser walked past. Reading it is necessary and not sufficient: the binary synthesises that key behind its own remote config (IUt(limits, jJe())returns[]whentengu_usage_overage_included_modelsis empty, and the key is spliced in only when the projection yielded something), so a--printsession never received it. The rawrate_limits.limits[]array it projects from does arrive untouched, soweekly_scopedentries naming a model are read from there too — the binary's filter, without its allowlist, since that list selects overage billing, not which limits meter you.resets_atis epoch seconds there as often as a string, so it is normalised rather than deserialized (a numeric one would have dropped the window in silence).loadPlanRateLimitsgives the usage endpoint 5 s and falls back toseedUtilization(), an object rebuilt from the rate-limit response headers that structurally carries onlyfive_hour/seven_day, flagged"seeded"and accepted identically downstream. A failed poll was therefore indistinguishable from one saying the per-model window is gone. A refresh is now merged into the last one by window key.set_modelas a correlated request, so a refusal restores the previous model),nimbus_quillhidden on both ingestion paths, and quota notifications titled throughUsageWindow.title(key).Evidence
Every claim about the binary was read out of
claude2.1.223, not inferred, and confirmed live againstidea.log:(that diagnostic log was temporary and is not in this branch)
Gates — run clean locally (
clean … --rerun-tasks)testkoverVerify/detekt/spotlessCheckverifyPluginbuildPluginnode_modulesentries,META-INF/{LICENSE,THIRD-PARTY-NOTICES.md}presentnpm test/ lint / formatnpm audit --omit=devManually validated in a real IDE (PyCharm 2026.2) before release.
🤖 Generated with Claude Code