mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 20:50:34 +00:00
feat(core, cli): add rate limit throttling retry with countdown UI
- Refactor retry utility to support GLM rate limit errors (code 1302) and TPM throttling - Add getRateLimitRetryInfo() for unified rate-limit error detection - Add exponential backoff for non-TPM rate limit errors - Extend StreamEventType.RETRY with RetryInfo payload for UI feedback - Add RetryCountdownMessage component for visual retry countdown - Update useGeminiStream hook to handle retry events with countdown timer - Add i18n support for rate limit messages (en/zh)
This commit is contained in:
parent
2394d732c3
commit
3fb641ca1a
12 changed files with 796 additions and 42 deletions
|
|
@ -65,6 +65,7 @@ import path from 'node:path';
|
|||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
const debugLogger = createDebugLogger('GEMINI_STREAM');
|
||||
|
||||
|
|
@ -125,6 +126,13 @@ export const useGeminiStream = (
|
|||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
const [pendingRetryErrorItem, setPendingRetryErrorItem] =
|
||||
useState<HistoryItemWithoutId | null>(null);
|
||||
const [pendingRetryCountdownItem, setPendingRetryCountdownItem] =
|
||||
useState<HistoryItemWithoutId | null>(null);
|
||||
const retryCountdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(
|
||||
null,
|
||||
);
|
||||
const processedMemoryToolsRef = useRef<Set<string>>(new Set());
|
||||
const {
|
||||
startNewPrompt,
|
||||
|
|
@ -189,6 +197,67 @@ export const useGeminiStream = (
|
|||
onComplete: (result: { userSelection: 'disable' | 'keep' }) => void;
|
||||
} | null>(null);
|
||||
|
||||
const stopRetryCountdownTimer = useCallback(() => {
|
||||
if (retryCountdownTimerRef.current) {
|
||||
clearInterval(retryCountdownTimerRef.current);
|
||||
retryCountdownTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearRetryCountdown = useCallback(() => {
|
||||
stopRetryCountdownTimer();
|
||||
setPendingRetryErrorItem(null);
|
||||
setPendingRetryCountdownItem(null);
|
||||
}, [stopRetryCountdownTimer]);
|
||||
|
||||
const startRetryCountdown = useCallback(
|
||||
(retryInfo: {
|
||||
reason: string;
|
||||
attempt: number;
|
||||
maxRetries: number;
|
||||
delayMs: number;
|
||||
}) => {
|
||||
stopRetryCountdownTimer();
|
||||
const startTime = Date.now();
|
||||
const { reason, attempt, maxRetries, delayMs } = retryInfo;
|
||||
|
||||
// Error line stays static (red with ✕ prefix)
|
||||
setPendingRetryErrorItem({
|
||||
type: MessageType.ERROR,
|
||||
text: t('Rate limit error: {{reason}}', { reason }),
|
||||
});
|
||||
|
||||
// Countdown line updates every second (dim/secondary color)
|
||||
const updateCountdown = () => {
|
||||
const elapsedMs = Date.now() - startTime;
|
||||
const remainingMs = Math.max(0, delayMs - elapsedMs);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
|
||||
setPendingRetryCountdownItem({
|
||||
type: 'retry_countdown',
|
||||
text: t(
|
||||
'Retrying in {{seconds}} seconds… (attempt {{attempt}}/{{maxRetries}})',
|
||||
{
|
||||
seconds: String(remainingSec),
|
||||
attempt: String(attempt),
|
||||
maxRetries: String(maxRetries),
|
||||
},
|
||||
),
|
||||
} as HistoryItemWithoutId);
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
stopRetryCountdownTimer();
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
retryCountdownTimerRef.current = setInterval(updateCountdown, 1000);
|
||||
},
|
||||
[stopRetryCountdownTimer],
|
||||
);
|
||||
|
||||
useEffect(() => () => stopRetryCountdownTimer(), [stopRetryCountdownTimer]);
|
||||
|
||||
const onExec = useCallback(async (done: Promise<void>) => {
|
||||
setIsResponding(true);
|
||||
await done;
|
||||
|
|
@ -295,6 +364,7 @@ export const useGeminiStream = (
|
|||
Date.now(),
|
||||
);
|
||||
setPendingHistoryItem(null);
|
||||
clearRetryCountdown();
|
||||
onCancelSubmit();
|
||||
setIsResponding(false);
|
||||
setShellInputFocused(false);
|
||||
|
|
@ -305,6 +375,7 @@ export const useGeminiStream = (
|
|||
onCancelSubmit,
|
||||
pendingHistoryItemRef,
|
||||
setShellInputFocused,
|
||||
clearRetryCountdown,
|
||||
config,
|
||||
getPromptCount,
|
||||
]);
|
||||
|
|
@ -609,10 +680,17 @@ export const useGeminiStream = (
|
|||
{ type: MessageType.INFO, text: 'User cancelled the request.' },
|
||||
userMessageTimestamp,
|
||||
);
|
||||
clearRetryCountdown();
|
||||
setIsResponding(false);
|
||||
setThought(null); // Reset thought when user cancels
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, setThought],
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
setThought,
|
||||
clearRetryCountdown,
|
||||
],
|
||||
);
|
||||
|
||||
const handleErrorEvent = useCallback(
|
||||
|
|
@ -631,9 +709,17 @@ export const useGeminiStream = (
|
|||
},
|
||||
userMessageTimestamp,
|
||||
);
|
||||
clearRetryCountdown();
|
||||
setThought(null); // Reset thought when there's an error
|
||||
},
|
||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, config, setThought],
|
||||
[
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
config,
|
||||
setThought,
|
||||
clearRetryCountdown,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCitationEvent = useCallback(
|
||||
|
|
@ -693,8 +779,9 @@ export const useGeminiStream = (
|
|||
userMessageTimestamp,
|
||||
);
|
||||
}
|
||||
clearRetryCountdown();
|
||||
},
|
||||
[addItem],
|
||||
[addItem, clearRetryCountdown],
|
||||
);
|
||||
|
||||
const handleChatCompressionEvent = useCallback(
|
||||
|
|
@ -853,7 +940,16 @@ export const useGeminiStream = (
|
|||
loopDetectedRef.current = true;
|
||||
break;
|
||||
case ServerGeminiEventType.Retry:
|
||||
// Will add the missing logic later
|
||||
// Clear any pending partial content from the failed attempt
|
||||
if (pendingHistoryItemRef.current) {
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
// Show retry info if available (rate-limit / throttling errors)
|
||||
if (event.retryInfo) {
|
||||
startRetryCountdown(event.retryInfo);
|
||||
} else {
|
||||
clearRetryCountdown();
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
// enforces exhaustive switch-case
|
||||
|
|
@ -878,7 +974,11 @@ export const useGeminiStream = (
|
|||
handleMaxSessionTurnsEvent,
|
||||
handleSessionTokenLimitExceededEvent,
|
||||
handleCitationEvent,
|
||||
startRetryCountdown,
|
||||
clearRetryCountdown,
|
||||
setThought,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -1216,10 +1316,18 @@ export const useGeminiStream = (
|
|||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() =>
|
||||
[pendingHistoryItem, pendingToolCallGroupDisplay].filter(
|
||||
(i) => i !== undefined && i !== null,
|
||||
),
|
||||
[pendingHistoryItem, pendingToolCallGroupDisplay],
|
||||
[
|
||||
pendingHistoryItem,
|
||||
pendingRetryErrorItem,
|
||||
pendingRetryCountdownItem,
|
||||
pendingToolCallGroupDisplay,
|
||||
].filter((i) => i !== undefined && i !== null),
|
||||
[
|
||||
pendingHistoryItem,
|
||||
pendingRetryErrorItem,
|
||||
pendingRetryCountdownItem,
|
||||
pendingToolCallGroupDisplay,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue