fix(core): classify auth-profile usage per rate-limit lane - #579
Merged
Conversation
usage_profile_health selected a single snapshot with an exact match on the `codex` limit id and discarded the rest, so a provider that bills several lanes against one profile was judged entirely on its default lane. Two consequences, only one of which is closed here. Auto-switch was silently disabled for any other lane. A usage limit reached on `codex_bengalfox` failed `usage_limit_matches_auto_switch_config`, which returns false rather than falling back to something safer, so the switch never fired at all. The gate now classifies against the lane the limit actually came from, and per-lane health is kept lane-first so a profile healthy on one lane is never compared against a profile healthy on another. Cooldowns are scoped the same way: a profile capped on one lane stays a legitimate target for a lane it has not spent. Health reported to the brokers is deliberately unchanged. usage_health_by_lane and usage_lane_availability are added so a caller can ask which lanes remain, but they return their own type rather than UsageProfileHealth, because a free sibling lane does not make the profile usable for work bound to the spent one. Wiring the display and dispatch paths to that answer needs the selection and apply types to carry a model, which they do not, and is not attempted here. Lanes are never merged and never maxed against each other; scoring the best lane would turn a false Exhausted into a false Healthy, which routes work into a refusal instead of away from one. Agent: Augustus
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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 this fixes
codex-rs/core/src/usage_profile_health.rsselected one rate-limit snapshot and threw the rest away:is_codex_limitmatcheslimit_id == "codex"exactly, so on a provider that bills several lanes against one auth profile every lane except the default was discarded.This is a second site with the same defect class as #578, not a duplicate of it. #578 fixed
auth_profile_usage.rs; this file was untouched by it (git show --stat 0f5990belists three files, none of them this one). The two have disjoint consumers — this one is whatapp-server's dispatch broker, the TUI broker andcore/src/session/auth_profile_auto_switch.rsread.Measured on this station, 23 targets with snapshots:
Zero profiles have both lanes spent.
Two symptoms; this PR closes one and deliberately leaves the other open
Closed here — auto-switch was silently disabled on any non-default lane. A usage limit reached on
codex_bengalfoxfailedusage_limit_matches_auto_switch_config, which returnsfalserather than degrading to something safer, sonext_profile_for_usage_limitreturnedNoneand the switch never fired at all. The gate now classifies against the lane the limit actually came from. Per-lane health is stored lane-first (lane -> profile -> health) so a profile healthy on one lane is never compared against a profile healthy on another, and cooldowns are scoped the same way — a profile capped on one lane stays a legitimate target for a lane it has not spent. That the cooldown key already carriedlimit_idis what suggests lane-awareness was the original intent and the predicate was the omission.Left open on purpose — the false
Exhaustedthat the brokers report. Acting on "free on the Spark lane" means routing work to a different model, and the types cannot express that:UsageProfileSelection.selected_profileis anOption<String>,next_profile_for_usage_limitreturnsOption<String>, andSessionSettingsUpdatehas nomodelfield at all. Flipping the verdict toHealthyis precisely the trap — measured on one profile in one window,-m gpt-5.3-codex-sparkreturns rc=0 while the default model returns rc=1 with a usage-limit message, so aHealthyverdict routes default-model work straight into the refusal. Health reported to the brokers is therefore byte-for-byte unchanged by this PR.usage_health_by_lane/usage_lane_availabilityare added as the query that can express it, and they return their own type rather thanUsageProfileHealthso the substitution cannot be made by accident. Wiring the display and dispatch paths to that answer needs the selection and apply types to carry a model; that is a separate change.Lanes are never merged and never maxed against each other. Prefix matching (
codex*) was rejected: it breaks the existingusage_health_treats_non_codex_limit_id_as_authoritativetest, and withcodexat 100% andcodex_bengalfoxbelow 34%, whichever lane.find()reached first would decide the verdict.Tests
Regression-first. Both reds were observed before any implementation existed.
Assertion-level red against the existing API, with a positive control passing in the same run:
Two-sided fixtures, both required by acceptance:
usage_health_by_lane_names_the_lane_that_is_still_usable— codex at 100%,codex_bengalfoxat 0% →Usable { usable_lanes: ["codex_bengalfox"] }.usage_lane_availability_reports_exhausted_only_when_every_lane_is_exhausted— all lanes at 100% →Exhausted { retry_at: Some(900) }. This is the negative control, and it exists as a fixture because it cannot be run live: no profile on this fleet currently has every lane spent.usage_health_by_lane_leaves_a_single_lane_profile_unchanged— a one-lane account classifies exactly as the legacy call, asserted againstusage_health_for_snapshotsitself rather than a copied literal.usage_health_for_snapshots_scores_only_the_default_lane— the anti-over-correction guard: the default query must still answerExhaustedfor a profile whose default lane is spent.usage_health_by_lane_reports_unknown_for_a_stale_read.profile_health_is_recorded_under_the_lane_the_limit_came_from,cooldown_on_one_lane_does_not_exclude_the_profile_from_another_lane.The two pre-existing guards that pin the exact-match rule —
usage_health_treats_non_codex_limit_id_as_authoritativeandusage_health_ignores_unknown_non_codex_limit_ids— still pass unchanged.Full crate suite, run with the same
RUST_MIN_STACKthe repo's own CI sets:The one failure is
shell_snapshot::tests::snapshot_shell_does_not_inherit_stdin(Error: read stdin probe status / No such file or directory). Attribution was measured, not assumed: re-running it with both changed files reverted to the base commit reproduces it identically.Without
RUST_MIN_STACKthe suite aborts inagent::control::testswith a stack overflow. That is also pre-existing and environmental — reverting both changed files to base and rebuilding reproduces it, and nothing undercodex-rs/core/src/agent/references either changed module.No release build was run and the installed
codewithon this station is untouched.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.