fix(ai): ranked antigravity bottleneck counters first

The google-antigravity ranking strategy originally returned the most-pressured
counter as primary and the runner-up as secondary. AuthStorage compares the
secondary ranking metrics before primary, so credentials with a dangerous
bottleneck but a healthy runner-up counter could outrank credentials with
balanced headroom.

Return the bottleneck as secondary and the runner-up as primary, and update the
strategy contract test to lock the ordering invariant.

Fixes #2187
This commit is contained in:
roboomp
2026-06-09 13:06:00 +00:00
parent e64aa62682
commit c9c2da9c8a
2 changed files with 16 additions and 9 deletions
+9 -4
View File
@@ -312,9 +312,14 @@ const ANTIGRAVITY_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;
* Anthropic / OpenAI) per tier per window, and {@link fetchAntigravityUsage}
* sorts them ascending by `remainingFraction` — so `limits[0]` is always the
* most-pressured counter for the credential, and `limits[1]` (when present)
* is the next-most-pressured. Mapping those to {primary, secondary} lets the
* ranker compare two credentials on both their bottleneck counter and their
* runner-up before falling back to round-robin.
* is the next-most-pressured counter.
*
* `AuthStorage` compares the `secondary*` ranking metrics before `primary*`
* because other providers model a long-window budget as secondary. Antigravity
* does not expose a short/long split; every counter is a sibling bottleneck.
* Therefore the most-pressured counter goes in `secondary`, with the runner-up
* in `primary`, so proactive account selection always ranks the bottleneck
* before any healthier sibling counter.
*
* The Antigravity API exposes `resetTime` but not window duration, so the
* drain-rate calculation depends on `windowDefaults`. Antigravity quotas are
@@ -324,7 +329,7 @@ const ANTIGRAVITY_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;
*/
export const antigravityRankingStrategy: CredentialRankingStrategy = {
findWindowLimits(report) {
return { primary: report.limits[0], secondary: report.limits[1] };
return { primary: report.limits[1], secondary: report.limits[0] };
},
windowDefaults: {
primaryMs: ANTIGRAVITY_DAILY_WINDOW_MS,
@@ -257,18 +257,20 @@ describe("antigravity ranking strategy", () => {
};
}
it("maps the most-pressured counter to primary and the runner-up to secondary", () => {
it("maps the most-pressured counter to secondary because AuthStorage compares secondary first", () => {
// fetchAntigravityUsage sorts ascending by remainingFraction, so a real
// report's limits[0] is always the bottleneck — the ranking strategy
// MUST surface that as the primary signal, not the first model alphabetically.
// report's limits[0] is always the bottleneck. AuthStorage compares the
// secondary ranking metrics before primary, so Antigravity must put the
// bottleneck there; otherwise [5%, 90%] remaining can beat [40%, 40%]
// because the runner-up counter looks healthier.
const report = {
provider: "google-antigravity" as const,
fetchedAt: Date.now(),
limits: [makeLimit(0.05, "Anthropic"), makeLimit(0.4, "Google"), makeLimit(0.9, "OpenAI")],
};
const { primary, secondary } = antigravityRankingStrategy.findWindowLimits(report);
expect(primary?.label).toBe("Anthropic");
expect(secondary?.label).toBe("Google");
expect(secondary?.label).toBe("Anthropic");
expect(primary?.label).toBe("Google");
});
it("returns undefined windows when the credential has no usage limits", () => {