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 (