Skip to content

fix(core): classify auth-profile usage per rate-limit lane - #579

Merged
andrei-hasna merged 1 commit into
mainfrom
usage-profile-health-lanes
Aug 12, 2026
Merged

fix(core): classify auth-profile usage per rate-limit lane#579
andrei-hasna merged 1 commit into
mainfrom
usage-profile-health-lanes

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What this fixes

codex-rs/core/src/usage_profile_health.rs selected one rate-limit snapshot and threw the rest away:

let Some(snapshot) = snapshots.iter().find(|snapshot| is_codex_limit(snapshot)) else {
    return UsageProfileHealth::Unknown;
};

is_codex_limit matches limit_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 0f5990be lists three files, none of them this one). The two have disjoint consumers — this one is what app-server's dispatch broker, the TUI broker and core/src/session/auth_profile_auto_switch.rs read.

Measured on this station, 23 targets with snapshots:

limit_id          slot      label       n      min     max   n>=100
codex             primary   weekly     23    100.0   100.0       23
codex_bengalfox   primary   weekly     23      0.0    34.0        0

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_bengalfox failed usage_limit_matches_auto_switch_config, which returns false rather than degrading to something safer, so next_profile_for_usage_limit returned None and 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 carried limit_id is what suggests lane-awareness was the original intent and the predicate was the omission.

Left open on purpose — the false Exhausted that 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_profile is an Option<String>, next_profile_for_usage_limit returns Option<String>, and SessionSettingsUpdate has no model field at all. Flipping the verdict to Healthy is precisely the trap — measured on one profile in one window, -m gpt-5.3-codex-spark returns rc=0 while the default model returns rc=1 with a usage-limit message, so a Healthy verdict 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_availability are added as the query that can express it, and they return their own type rather than UsageProfileHealth so 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 existing usage_health_treats_non_codex_limit_id_as_authoritative test, and with codex at 100% and codex_bengalfox below 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:

test usage_profile_health::tests::usage_limit_matches_auto_switch_config_for_a_non_default_lane ... FAILED
assertion failed: usage_limit_matches_auto_switch_config(&config(), Some(&snapshot))
test result: FAILED. 21 passed; 1 failed; 0 ignored

Two-sided fixtures, both required by acceptance:

  • usage_health_by_lane_names_the_lane_that_is_still_usable — codex at 100%, codex_bengalfox at 0% → Usable { usable_lanes: ["codex_bengalfox"] }.
  • usage_lane_availability_reports_exhausted_only_when_every_lane_is_exhaustedall 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 against usage_health_for_snapshots itself rather than a copied literal.
  • usage_health_for_snapshots_scores_only_the_default_lane — the anti-over-correction guard: the default query must still answer Exhausted for 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_authoritative and usage_health_ignores_unknown_non_codex_limit_ids — still pass unchanged.

cargo test -p codex-core --lib usage_profile_health::
test result: ok. 26 passed; 0 failed; 0 ignored

cargo test -p codex-core --lib auth_profile_auto_switch::
test result: ok. 6 passed; 0 failed; 0 ignored

Full crate suite, run with the same RUST_MIN_STACK the repo's own CI sets:

RUST_MIN_STACK=8388608 cargo test -p codex-core --lib
test result: FAILED. 2318 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out

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_STACK the suite aborts in agent::control::tests with a stack overflow. That is also pre-existing and environmental — reverting both changed files to base and rebuilding reproduces it, and nothing under codex-rs/core/src/agent/ references either changed module.

No release build was run and the installed codewith on this station is untouched.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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
@andrei-hasna
andrei-hasna merged commit 3271c40 into main Aug 12, 2026
27 of 28 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant