From fba389a3458438ca9ca13606e52e48093ebc80fd Mon Sep 17 00:00:00 2001 From: maoxin1234 <875408344@qq.com> Date: Mon, 14 Sep 2026 09:35:46 +0800 Subject: [PATCH 1/7] feat(account-pool): display threshold summary with strategy and warn on drained switch (#4521, #4524) --- .../AccountPoolStrategyControls.tsx | 21 ++++++ gui/src/components/CodexAccountPool.tsx | 2 + .../components/CodexPoolStrategySetting.tsx | 15 +++- .../components/codex-account-switch-modal.tsx | 10 +++ gui/src/i18n/de.ts | 5 ++ gui/src/i18n/en.ts | 5 ++ gui/src/i18n/fr.ts | 5 ++ gui/src/i18n/ja.ts | 5 ++ gui/src/i18n/ko.ts | 5 ++ gui/src/i18n/ru.ts | 5 ++ gui/src/i18n/tr.ts | 5 ++ gui/src/i18n/zh-TW.ts | 5 ++ gui/src/i18n/zh.ts | 5 ++ gui/src/styles.css | 15 ++++ gui/tests/account-pool-strategy.test.tsx | 72 +++++++++++++++++++ src/cli/account-extended.ts | 20 +++++- 16 files changed, 197 insertions(+), 3 deletions(-) diff --git a/gui/src/components/AccountPoolStrategyControls.tsx b/gui/src/components/AccountPoolStrategyControls.tsx index 2a813bcc4e..17c95bfd97 100644 --- a/gui/src/components/AccountPoolStrategyControls.tsx +++ b/gui/src/components/AccountPoolStrategyControls.tsx @@ -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,20 @@ 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"); + } + if (threshold > 0) { + if (strategy === "fill-first") { + return t("accountPool.drainAtThreshold", { threshold: String(threshold) }); + } + return t("accountPool.switchAtThreshold", { threshold: String(threshold) }); + } + return t("accountPool.proactiveSwitchingOff"); + })(); + return (
{/* @@ -86,6 +102,11 @@ export default function AccountPoolStrategyControls({ label={t("accountPool.strategy")} onChange={(next) => onStrategyChange(next as AccountPoolStrategy)} /> + {thresholdSummary && ( + + {thresholdSummary} + + )}
{strategy === "round-robin" && ( diff --git a/gui/src/components/CodexAccountPool.tsx b/gui/src/components/CodexAccountPool.tsx index c7e006be6a..5d3209390e 100644 --- a/gui/src/components/CodexAccountPool.tsx +++ b/gui/src/components/CodexAccountPool.tsx @@ -546,6 +546,7 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban subscribeLoadObserver={controller.subscribeLoadObserver} readLastActive={controller.readLastActive} onStrategyResolved={setPoolStrategy} + threshold={autoSwitch.threshold} /> setConfirm(null)} onConfirm={() => { void setActive(confirm.id === "__main__" ? "__main__" : confirm.id); }} /> diff --git a/gui/src/components/CodexPoolStrategySetting.tsx b/gui/src/components/CodexPoolStrategySetting.tsx index 6e44d793ce..2932073867 100644 --- a/gui/src/components/CodexPoolStrategySetting.tsx +++ b/gui/src/components/CodexPoolStrategySetting.tsx @@ -16,13 +16,16 @@ import type { CodexAccountLoadObserver } from "../hooks/useCodexAccountPool"; function strategyFieldsFromActive(value: unknown): { strategy: AccountPoolStrategy; stickyLimit: number; + threshold?: number; } | null { if (!value || typeof value !== "object") return null; const row = value as Record; - if (!("accountPoolStrategy" in row) && !("accountPoolStickyLimit" in row)) return null; + if (!("accountPoolStrategy" in row) && !("accountPoolStickyLimit" in row) && !("autoSwitchThreshold" in row)) return null; + const threshold = typeof row.autoSwitchThreshold === "number" ? row.autoSwitchThreshold : undefined; return { strategy: normalizeAccountPoolStrategy(row.accountPoolStrategy), stickyLimit: normalizeAccountPoolStickyLimit(row.accountPoolStickyLimit), + threshold, }; } @@ -36,17 +39,20 @@ export default function CodexPoolStrategySetting({ subscribeLoadObserver, readLastActive, onStrategyResolved, + threshold: propThreshold, }: { apiBase: string; subscribeLoadObserver?: (observer: CodexAccountLoadObserver) => () => void; readLastActive?: () => unknown; onStrategyResolved?: (strategy: AccountPoolStrategy) => void; + threshold?: number; }) { const t = useT(); // Seed defaults immediately — never gate the control chrome on a network round-trip. const [strategy, setStrategy] = useState(DEFAULT_ACCOUNT_POOL_STRATEGY); const [stickyLimit, setStickyLimit] = useState(DEFAULT_ACCOUNT_POOL_STICKY_LIMIT); const [stickyDraft, setStickyDraft] = useState(String(DEFAULT_ACCOUNT_POOL_STICKY_LIMIT)); + const [serverThreshold, setServerThreshold] = useState(undefined); const [hydrated, setHydrated] = useState(false); const hydratedRef = useRef(false); const [saving, setSaving] = useState(false); @@ -60,9 +66,13 @@ export default function CodexPoolStrategySetting({ const applyServer = useCallback((json: { accountPoolStrategy?: unknown; accountPoolStickyLimit?: unknown; + autoSwitchThreshold?: unknown; }) => { const nextStrategy = normalizeAccountPoolStrategy(json.accountPoolStrategy); const nextSticky = normalizeAccountPoolStickyLimit(json.accountPoolStickyLimit); + if (typeof json.autoSwitchThreshold === "number") { + setServerThreshold(json.autoSwitchThreshold); + } setStrategy(nextStrategy); onStrategyResolved?.(nextStrategy); setStickyLimit(nextSticky); @@ -79,6 +89,7 @@ export default function CodexPoolStrategySetting({ applyServer({ accountPoolStrategy: fields.strategy, accountPoolStickyLimit: fields.stickyLimit, + autoSwitchThreshold: fields.threshold, }); }, [applyServer]); @@ -89,6 +100,7 @@ export default function CodexPoolStrategySetting({ const payload = await res.json() as { accountPoolStrategy?: unknown; accountPoolStickyLimit?: unknown; + autoSwitchThreshold?: unknown; }; // A save started while this GET was in flight — retry once after it settles. if (savingRef.current) { @@ -215,6 +227,7 @@ export default function CodexPoolStrategySetting({ void; onConfirm: () => void; }) { @@ -39,6 +42,8 @@ export function CodexAccountSwitchModal({ onCancel(); }, [onCancel]); + const exceedsThreshold = threshold !== undefined && threshold > 0 && maxQuotaUtilisation(confirm.quota) >= threshold; + return ( {t("codexAuth.cacheWarning")} )} + {exceedsThreshold && ( +
+ {t("codexAuth.switchExceedsThresholdWarning", { threshold })} +
+ )}