-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(account-pool): show what the usage threshold means per strategy #4982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f36a6e
1782dc3
833350c
f39ee9c
ee2d17c
a5ee5df
d011e90
3e2f342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { CODEX_EXHAUSTED_USAGE_PERCENT, TERMINAL_SHORT_WINDOW_FRESHNESS_MS } from "../../src/codex/quota-types"; | ||
|
|
||
| export interface AccountQuota { | ||
| weeklyPercent?: number; | ||
| fiveHourPercent?: number; | ||
|
|
@@ -7,6 +9,8 @@ export interface AccountQuota { | |
| weeklyResetAt?: number; | ||
| fiveHourResetAt?: number; | ||
| shortResetAt?: number; | ||
| /** Local observation time for the short-window percentage. */ | ||
| shortObservedAt?: number; | ||
| shortWindowSeconds?: number; | ||
| monthlyResetAt?: number; | ||
| customWindows?: { label: string; percent: number; resetAt?: number }[]; | ||
|
|
@@ -53,3 +57,44 @@ export function normalizeQuotaForPlan(quota: AccountQuota | null, plan: string | | |
| updatedAt: normalized.updatedAt, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Compute the governing Codex usage score matching the server's auto-switch threshold evaluation. | ||
| * | ||
| * Evaluates governing quota windows based on the account's plan: | ||
| * - For 30-day only plans (e.g. Free/Go), only the monthly window governs. | ||
| * - For standard plans, weekly and monthly windows govern. | ||
| * - A known five-hour / short window refines a known governing long-window score. | ||
| * - If no long window has been observed, an active terminal short burst (at 100%) acts as exhausted (100). | ||
| * - Unknown or unprimed quota returns `null` so callers do not spuriously trigger threshold actions. | ||
| */ | ||
| export function computeCodexUsageScore( | ||
| quota: AccountQuota | null | undefined, | ||
| plan?: string | null, | ||
| now: number = Date.now(), | ||
| ): number | null { | ||
| if (!quota) return null; | ||
| const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value); | ||
| const shortPercent = finite(quota.fiveHourPercent) | ||
| ? quota.fiveHourPercent | ||
| : (finite(quota.shortPercent) ? quota.shortPercent : undefined); | ||
| const longWindows = isThirtyDayOnlyPlan(plan) | ||
| ? [quota.monthlyPercent] | ||
| : [quota.weeklyPercent, quota.monthlyPercent]; | ||
| const knownLong = longWindows.filter(finite); | ||
| if (knownLong.length === 0) { | ||
| const shortReset = quota.fiveHourResetAt ?? quota.shortResetAt; | ||
| const shortObservationAge = typeof quota.shortObservedAt === "number" | ||
| ? now - quota.shortObservedAt | ||
| : undefined; | ||
| const isExhausted = finite(shortPercent) && shortPercent >= CODEX_EXHAUSTED_USAGE_PERCENT && ( | ||
| (typeof shortReset === "number" && shortReset > now) || | ||
| (typeof shortObservationAge === "number" | ||
|
Comment on lines
+90
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a short-only terminal quota carries AGENTS.md reference: gui/AGENTS.md:L9-L10 Useful? React with 👍 / 👎. |
||
| && shortObservationAge >= 0 | ||
| && shortObservationAge <= TERMINAL_SHORT_WINDOW_FRESHNESS_MS) | ||
| ); | ||
| return isExhausted ? 100 : null; | ||
| } | ||
| const values = finite(shortPercent) ? [...knownLong, shortPercent] : knownLong; | ||
| return values.length ? Math.max(...values) : null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ const STRATEGY_HINT_KEYS = { | |
| export interface AccountPoolStrategyControlsProps { | ||
| strategy: AccountPoolStrategy; | ||
| codex?: boolean; | ||
| threshold?: number; | ||
| stickyDraft: string; | ||
| disabled?: boolean; | ||
| strategySelectId?: string; | ||
|
|
@@ -45,6 +46,7 @@ export interface AccountPoolStrategyControlsProps { | |
| export default function AccountPoolStrategyControls({ | ||
| strategy, | ||
| codex = false, | ||
| threshold, | ||
| stickyDraft, | ||
| disabled = false, | ||
| strategySelectId = "account-pool-strategy", | ||
|
|
@@ -59,6 +61,23 @@ export default function AccountPoolStrategyControls({ | |
| label: t(STRATEGY_LABEL_KEYS[value]), | ||
| })); | ||
|
|
||
| const thresholdSummary = (() => { | ||
| if (threshold === undefined) return null; | ||
| if (strategy === "round-robin") { | ||
| return t("accountPool.thresholdNotUsed"); | ||
|
Comment on lines
+66
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When round-robin is selected, the threshold still releases an operator's manual pin: AGENTS.md reference: gui/AGENTS.md:L9-L10 Useful? React with 👍 / 👎. |
||
| } | ||
| if (threshold > 0) { | ||
| if (strategy === "fill-first") { | ||
| return t("accountPool.drainAtThreshold", { threshold: String(threshold) }); | ||
| } | ||
| if (strategy === "reset-first") { | ||
| return t("accountPool.resetBelowThreshold", { threshold: String(threshold) }); | ||
| } | ||
| return t("accountPool.switchAtThreshold", { threshold: String(threshold) }); | ||
|
Comment on lines
+69
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The change adds strategy-specific threshold explanations and a manual-switch warning, but the commit updates only internal AGENTS.md reference: gui/AGENTS.md:L31-L36 Useful? React with 👍 / 👎. |
||
| } | ||
| return t("accountPool.proactiveSwitchingOff"); | ||
| })(); | ||
|
|
||
| return ( | ||
| <div className="account-pool-strategy-controls"> | ||
| {/* | ||
|
|
@@ -86,6 +105,11 @@ export default function AccountPoolStrategyControls({ | |
| label={t("accountPool.strategy")} | ||
| onChange={(next) => onStrategyChange(next as AccountPoolStrategy)} | ||
| /> | ||
| {thresholdSummary && ( | ||
| <span className="badge badge-muted account-pool-threshold-badge" data-testid="account-pool-threshold-summary"> | ||
| {thresholdSummary} | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| {strategy === "round-robin" && ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1917,6 +1917,7 @@ export const fr: Record<TKey, string> = { | |
| "codexAuth.switchTitle": "Changer de compte actif ?", | ||
| "codexAuth.switchDesc": "Prend effet immédiatement. Les fils liés à un compte et les requêtes déjà en cours conservent le compte capturé ; les requêtes nouvelles ou non liées utilisent le niveau d’ordre du compte sélectionné, et les comptes de même ordre continuent d’alterner.", | ||
| "codexAuth.cacheWarning": "Le cache des prompts est réinitialisé lors d’un changement de compte. La nouvelle session démarre avec un cache vide.", | ||
| "codexAuth.switchExceedsThresholdWarning": "Ce compte a atteint ou dépassé le seuil de basculement ({threshold} %). La sélection épinglée sera libérée si la marge de quota est insuffisante.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Run the required GUI checks before marking this change complete. The new translation entries in 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| "codexAuth.setAsNext": "Utiliser ensuite ce compte", | ||
| "codexAuth.cancel": "Annuler", | ||
| "codexAuth.switchBack": "Revenir au compte principal ?", | ||
|
|
@@ -1986,6 +1987,11 @@ export const fr: Record<TKey, string> = { | |
| "accountPool.stickyLimitInvalid": "Saisissez un nombre entier compris entre 1 et 100", | ||
| "accountPool.strategyLoadFailed": "Impossible de charger la stratégie de rotation.", | ||
| "accountPool.strategyUpdateFailed": "Impossible d’enregistrer la stratégie de rotation.", | ||
| "accountPool.switchAtThreshold": "bascule à {threshold} %", | ||
| "accountPool.drainAtThreshold": "épuisement à {threshold} %", | ||
| "accountPool.resetBelowThreshold": "prochaine réinitialisation sous {threshold} %", | ||
| "accountPool.thresholdNotUsed": "seuil non utilisé", | ||
| "accountPool.proactiveSwitchingOff": "bascule proactive désactivée", | ||
| "accountPool.quotaWindow": "Fenêtre de quota", | ||
| "accountPool.quotaWindowDesc": "Barre d’utilisation en cache qui régit la sélection des nouvelles sessions par quota, les seuils de remplissage prioritaire et les remplacements 429 admissibles.", | ||
| "accountPool.quotaWindowFiveHour": "Barre de 5 heures", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reset-less branch depends on
shortObservedAt, but the management payload does not always expose it:mainQuotaWithCarriedResetCreditsbuilds the main row from the raw parse without that local timestamp, andquotaForPlandrops it from Free/Go pool rows. For those accounts, routing can classify a freshly observed reset-less 100% short window as exhausted while this function always returnsnull, so the promised pre-switch warning is missing. Project the stored observation timestamp into these DTOs, or return a server-computed usage score.AGENTS.md reference: gui/AGENTS.md:L9-L10
Useful? React with 👍 / 👎.