feat(retry-status): generalize rate-limit UI to generic retry/backoff with cause

- Replace rateLimitRetrySchema with retryStatusSchema including cause and rateLimitSeconds fields
- Add new RetryStatusRow component with differentiated messaging:
  - Rate limit: 'Rate limit set to {{rateLimitSeconds}} seconds' (title) + 'Waiting {{seconds}} seconds' (subtitle)
  - Retry: Error message (title) + 'Trying in {{seconds}} seconds (attempt #{{attempt}})' (subtitle)
- Update Task.say emitters to use retryStatus metadata with appropriate cause:
  - 'rate_limit' for provider window waits and 429 errors (includes rateLimitSeconds)
  - 'backoff' for exponential backoff retries
- Migrate i18n keys from rateLimitRetry to retryStatus with new nested structure in English
- Maintain backward compatibility with legacy rateLimitRetry metadata
- Update tests to cover new RetryStatusRow component and metadata structure
This commit is contained in:
daniel-lxs 2025-11-03 18:45:05 -05:00
parent 7b46d60901
commit 1a41005466
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
25 changed files with 523 additions and 33 deletions

View file

@ -197,16 +197,26 @@ export const contextCondenseSchema = z.object({
export type ContextCondense = z.infer<typeof contextCondenseSchema>
/**
* RateLimitRetryMetadata
* RetryStatusMetadata
*/
export const rateLimitRetrySchema = z.object({
type: z.literal("rate_limit_retry"),
export const retryStatusSchema = z.object({
type: z.literal("retry_status"),
status: z.enum(["waiting", "retrying", "cancelled"]),
remainingSeconds: z.number().optional(),
attempt: z.number().optional(),
maxAttempts: z.number().optional(),
origin: z.enum(["pre_request", "retry_attempt"]).optional(),
detail: z.string().optional(),
cause: z.enum(["rate_limit", "backoff"]),
rateLimitSeconds: z.number().optional(), // The original rate limit setting (for displaying "Rate limit set to X seconds")
})
export type RetryStatusMetadata = z.infer<typeof retryStatusSchema>
// Keep legacy type for backward compatibility during migration
export const rateLimitRetrySchema = retryStatusSchema.extend({
type: z.literal("rate_limit_retry"),
cause: z.literal("rate_limit").default("rate_limit"),
})
export type RateLimitRetryMetadata = z.infer<typeof rateLimitRetrySchema>
@ -238,7 +248,8 @@ export const clineMessageSchema = z.object({
previous_response_id: z.string().optional(),
})
.optional(),
rateLimitRetry: rateLimitRetrySchema.optional(),
retryStatus: retryStatusSchema.optional(),
rateLimitRetry: rateLimitRetrySchema.optional(), // Legacy field for backward compatibility
})
.optional(),
})

View file

@ -123,6 +123,19 @@ const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
const FORCED_CONTEXT_REDUCTION_PERCENT = 75 // Keep 75% of context (remove 25%) on context window errors
const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors
interface RetryStatusPayload {
type: "retry_status"
status: "waiting" | "retrying" | "cancelled"
remainingSeconds?: number
attempt?: number
maxAttempts?: number
origin: "pre_request" | "retry_attempt"
detail?: string
cause: "rate_limit" | "backoff"
rateLimitSeconds?: number
}
// Legacy interface for backward compatibility
interface RateLimitRetryPayload {
type: "rate_limit_retry"
status: "waiting" | "retrying" | "cancelled"
@ -1110,7 +1123,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (partial !== undefined) {
const lastMessage = this.clineMessages.at(-1)
const isRateLimitUpdate = type === "api_req_retry_delayed" && options.metadata?.rateLimitRetry !== undefined
const isRateLimitUpdate =
type === "api_req_retry_delayed" &&
(options.metadata?.retryStatus !== undefined || options.metadata?.rateLimitRetry !== undefined)
const isUpdatingPreviousPartial =
lastMessage &&
lastMessage.type === "say" &&
@ -2677,54 +2692,70 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
let rateLimitDelay = 0
const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise<void> => {
const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise<void> => {
await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, {
isNonInteractive: true,
metadata: { rateLimitRetry: payload },
metadata: {
retryStatus: payload,
rateLimitRetry:
payload.cause === "rate_limit" ? { ...payload, type: "rate_limit_retry" as const } : undefined,
},
})
}
// Legacy function for backward compatibility
const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise<void> => {
const retryPayload: RetryStatusPayload = { ...payload, type: "retry_status", cause: "rate_limit" }
await sendRetryStatusUpdate(retryPayload, isPartial)
}
const runRateLimitCountdown = async ({
seconds,
origin,
attempt,
maxAttempts,
detail,
rateLimitSeconds,
}: {
seconds: number
origin: RateLimitRetryPayload["origin"]
origin: RetryStatusPayload["origin"]
attempt?: number
maxAttempts?: number
detail?: string
rateLimitSeconds?: number
}): Promise<boolean> => {
const normalizedSeconds = Math.max(0, Math.ceil(seconds))
if (normalizedSeconds <= 0) {
if (this.abort) {
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "cancelled",
remainingSeconds: 0,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
false,
)
return false
}
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "retrying",
remainingSeconds: 0,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
false,
)
@ -2733,30 +2764,34 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
for (let i = normalizedSeconds; i > 0; i--) {
if (this.abort) {
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "cancelled",
remainingSeconds: i,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
false,
)
return false
}
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "waiting",
remainingSeconds: i,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
true,
)
@ -2765,30 +2800,34 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
if (this.abort) {
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "cancelled",
remainingSeconds: 0,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
false,
)
return false
}
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "retrying",
remainingSeconds: 0,
attempt,
maxAttempts,
origin,
detail,
cause: "rate_limit",
rateLimitSeconds,
},
false,
)
@ -2807,10 +2846,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there.
if (rateLimitDelay > 0 && retryAttempt === 0) {
const rateLimit = apiConfiguration?.rateLimitSeconds || 0
const countdownCompleted = await runRateLimitCountdown({
seconds: rateLimitDelay,
origin: "pre_request",
attempt: 1,
rateLimitSeconds: rateLimit,
})
if (!countdownCompleted) {
@ -3086,40 +3127,57 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}` : firstLine
})()
// Helper to send rate limit updates with structured metadata
const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise<void> => {
// Helper to send retry status updates with structured metadata
const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise<void> => {
await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, {
isNonInteractive: true,
metadata: { rateLimitRetry: payload },
metadata: {
retryStatus: payload,
rateLimitRetry:
payload.cause === "rate_limit"
? { ...payload, type: "rate_limit_retry" as const }
: undefined,
},
})
}
// Determine the cause based on error type
const cause: "rate_limit" | "backoff" = error?.status === 429 ? "rate_limit" : "backoff"
// For rate limit errors, include the rate limit setting
const rateLimitSetting = state?.apiConfiguration?.rateLimitSeconds || 0
const rateLimitSeconds = cause === "rate_limit" ? rateLimitSetting : undefined
// Show countdown timer with exponential backoff using structured metadata
for (let i = finalDelay; i > 0; i--) {
// Check abort flag during countdown to allow early exit
if (this.abort) {
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "cancelled",
remainingSeconds: i,
attempt: retryAttempt + 1,
origin: "retry_attempt",
detail: sanitizedDetail,
cause,
rateLimitSeconds,
},
false,
)
throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`)
}
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "waiting",
remainingSeconds: i,
attempt: retryAttempt + 1,
origin: "retry_attempt",
detail: sanitizedDetail,
cause,
rateLimitSeconds,
},
true,
)
@ -3128,28 +3186,32 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Final check before retrying
if (this.abort) {
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "cancelled",
remainingSeconds: 0,
attempt: retryAttempt + 1,
origin: "retry_attempt",
detail: sanitizedDetail,
cause,
rateLimitSeconds,
},
false,
)
throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`)
}
await sendRateLimitUpdate(
await sendRetryStatusUpdate(
{
type: "rate_limit_retry",
type: "retry_status",
status: "retrying",
remainingSeconds: 0,
attempt: retryAttempt + 1,
origin: "retry_attempt",
detail: sanitizedDetail,
cause,
rateLimitSeconds,
},
false,
)

View file

@ -44,7 +44,9 @@ import { appendImages } from "@src/utils/imageUtils"
import { McpExecution } from "./McpExecution"
import { ChatTextArea } from "./ChatTextArea"
import { RateLimitRetryRow } from "./RateLimitRetryRow"
import { RetryStatusRow } from "./RetryStatusRow"
export { RateLimitRetryRow } from "./RateLimitRetryRow"
export { RetryStatusRow } from "./RetryStatusRow"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import { useSelectedModel } from "../ui/hooks/useSelectedModel"
import {
@ -1178,7 +1180,13 @@ export const ChatRowContent = ({
// Prevent multiple blocks returning, we only need a single block
// that's constantly updated
if (!isLast) return null
return <RateLimitRetryRow metadata={message.metadata?.rateLimitRetry} />
// Use new RetryStatusRow if retryStatus metadata is available, fall back to legacy
return message.metadata?.retryStatus ? (
<RetryStatusRow metadata={message.metadata.retryStatus} />
) : (
<RateLimitRetryRow metadata={message.metadata?.rateLimitRetry} />
)
case "shell_integration_warning":
return <CommandExecutionError />
case "checkpoint_saved":

View file

@ -428,7 +428,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// an "ask" while ask is waiting for response.
switch (lastMessage.say) {
case "api_req_retry_delayed": {
if (lastMessage.metadata?.rateLimitRetry?.status === "cancelled") {
const retryStatus =
lastMessage.metadata?.retryStatus || lastMessage.metadata?.rateLimitRetry
if (retryStatus?.status === "cancelled") {
setSendingDisabled(false)
} else {
setSendingDisabled(true)

View file

@ -0,0 +1,96 @@
import React, { useMemo } from "react"
import { useTranslation } from "react-i18next"
import type { RetryStatusMetadata } from "@roo-code/types"
import { ProgressIndicator } from "./ProgressIndicator"
export interface RetryStatusRowProps {
metadata?: RetryStatusMetadata
}
export const RetryStatusRow = ({ metadata }: RetryStatusRowProps) => {
const { t } = useTranslation()
const title = useMemo(() => {
if (!metadata) {
return ""
}
const isRateLimit = metadata.cause === "rate_limit"
if (isRateLimit) {
// For rate limit, show "Rate limit set for Xs"
return t("chat:retryStatus.rateLimit.title", {
rateLimitSeconds: metadata.rateLimitSeconds || 30, // fallback to 30 if not provided
})
} else {
// For backoff/retry, show the error message if available
return metadata.detail || t("chat:retryStatus.backoff.title")
}
}, [metadata, t])
const subtitle = useMemo(() => {
if (!metadata) {
return ""
}
const isRateLimit = metadata.cause === "rate_limit"
if (metadata.status === "retrying") {
return isRateLimit ? t("chat:retryStatus.rateLimit.proceeding") : t("chat:retryStatus.backoff.retrying")
}
if (metadata.status === "cancelled") {
return isRateLimit ? t("chat:retryStatus.rateLimit.cancelled") : t("chat:retryStatus.backoff.cancelled")
}
if (typeof metadata.remainingSeconds === "number") {
if (isRateLimit) {
// Rate limit: just "Waiting 22s" (no attempt number)
return t("chat:retryStatus.rateLimit.waiting", {
seconds: metadata.remainingSeconds,
})
} else {
// Retry: "Trying in 22s (attempt #2)"
const baseKey = "chat:retryStatus.backoff"
if (metadata.attempt && metadata.maxAttempts) {
return t(`${baseKey}.waitingWithAttemptMax`, {
seconds: metadata.remainingSeconds,
attempt: metadata.attempt,
maxAttempts: metadata.maxAttempts,
})
}
if (metadata.attempt) {
return t(`${baseKey}.waitingWithAttempt`, {
seconds: metadata.remainingSeconds,
attempt: metadata.attempt,
})
}
return t(`${baseKey}.waiting`, { seconds: metadata.remainingSeconds })
}
}
return ""
}, [metadata, t])
const iconNode =
metadata?.status === "cancelled" ? (
<span className="codicon codicon-circle-slash text-vscode-descriptionForeground" aria-hidden="true"></span>
) : (
<ProgressIndicator />
)
return (
<div role="status" aria-live="polite" className="mt-1">
<div className="flex items-start gap-3 rounded-md border border-vscode-editorGroup-border/60 bg-vscode-editor-background px-4 py-3">
<div className="mt-0.5">{iconNode}</div>
<div className="flex flex-col gap-1 text-sm leading-5">
<span className="font-semibold text-vscode-foreground">{title}</span>
{subtitle && <span className="text-vscode-descriptionForeground">{subtitle}</span>}
</div>
</div>
</div>
)
}

View file

@ -27,6 +27,7 @@ describe("RateLimitRetryRow", () => {
attempt: 2,
maxAttempts: 5,
origin: "retry_attempt",
cause: "rate_limit",
}
render(<RateLimitRetryRow metadata={metadata} />)
@ -40,6 +41,7 @@ describe("RateLimitRetryRow", () => {
type: "rate_limit_retry",
status: "retrying",
origin: "retry_attempt",
cause: "rate_limit",
}
render(<RateLimitRetryRow metadata={metadata} />)
@ -53,6 +55,7 @@ describe("RateLimitRetryRow", () => {
type: "rate_limit_retry",
status: "cancelled",
origin: "retry_attempt",
cause: "rate_limit",
}
const { container } = render(<RateLimitRetryRow metadata={metadata} />)
@ -82,6 +85,7 @@ describe("RateLimitRetryRow", () => {
attempt: 1,
maxAttempts: 3,
origin: "retry_attempt",
cause: "rate_limit",
}
const { rerender } = render(<RateLimitRetryRow metadata={initialMetadata} />)
@ -94,6 +98,7 @@ describe("RateLimitRetryRow", () => {
type: "rate_limit_retry",
status: "retrying",
origin: "retry_attempt",
cause: "rate_limit",
}
rerender(<RateLimitRetryRow metadata={updatedMetadata} />)
@ -111,6 +116,7 @@ describe("RateLimitRetryRow", () => {
attempt: 1,
maxAttempts: 3,
origin: "retry_attempt",
cause: "rate_limit",
}
const { rerender } = render(<RateLimitRetryRow metadata={metadata1} />)

View file

@ -0,0 +1,133 @@
// npx vitest run src/components/chat/__tests__/RetryStatusRow.spec.tsx
import { render, screen } from "@/utils/test-utils"
import { RetryStatusRow } from "../ChatRow"
import type { RetryStatusMetadata } from "@roo-code/types"
vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => key,
}),
}))
// Manual trigger instructions (for developer reference):
// 1) In Roo Code settings, set your provider Rate limit seconds to a small value (e.g., 5s).
// 2) Send a message to start an API request.
// 3) Immediately send another message within the configured window.
// The pre-request wait will emit `api_req_retry_delayed` with metadata,
// rendering a single live status row: spinner + persecond countdown,
// then "Retrying now..." at zero. Input remains disabled during the wait.
describe("RetryStatusRow", () => {
it("renders waiting countdown with attempt and max attempts for rate limit cause", () => {
const metadata: RetryStatusMetadata = {
type: "retry_status",
status: "waiting",
remainingSeconds: 12,
attempt: 2,
maxAttempts: 5,
origin: "retry_attempt",
cause: "rate_limit",
}
render(<RetryStatusRow metadata={metadata} />)
expect(screen.getByText("chat:retryStatus.rateLimit.waitingWithAttemptMax")).toBeInTheDocument()
expect(screen.getByText("chat:retryStatus.rateLimit.description")).toBeInTheDocument()
})
it("renders retrying state for backoff cause", () => {
const metadata: RetryStatusMetadata = {
type: "retry_status",
status: "retrying",
origin: "retry_attempt",
cause: "backoff",
}
render(<RetryStatusRow metadata={metadata} />)
expect(screen.getByText("chat:retryStatus.backoff.retrying")).toBeInTheDocument()
})
it("renders cancelled state (neutral)", () => {
const metadata: RetryStatusMetadata = {
type: "retry_status",
status: "cancelled",
origin: "retry_attempt",
cause: "rate_limit",
}
const { container } = render(<RetryStatusRow metadata={metadata} />)
expect(screen.getByText("chat:retryStatus.rateLimit.cancelled")).toBeInTheDocument()
// Iconography: ensure neutral cancelled icon is present
const cancelledIcon = container.querySelector(".codicon-circle-slash")
expect(cancelledIcon).not.toBeNull()
})
it("renders empty description when metadata is missing", () => {
render(<RetryStatusRow />)
expect(screen.getByText("chat:retryStatus.backoff.waiting")).toBeInTheDocument()
})
it("updates when metadata changes from waiting to retrying", () => {
const initialMetadata: RetryStatusMetadata = {
type: "retry_status",
status: "waiting",
remainingSeconds: 5,
attempt: 1,
maxAttempts: 3,
origin: "retry_attempt",
cause: "backoff",
}
const { rerender } = render(<RetryStatusRow metadata={initialMetadata} />)
// Initial state: waiting
expect(screen.getByText("chat:retryStatus.backoff.waitingWithAttemptMax")).toBeInTheDocument()
// Update to retrying state
const updatedMetadata: RetryStatusMetadata = {
type: "retry_status",
status: "retrying",
origin: "retry_attempt",
cause: "backoff",
}
rerender(<RetryStatusRow metadata={updatedMetadata} />)
// Should now show retrying
expect(screen.getByText("chat:retryStatus.backoff.retrying")).toBeInTheDocument()
expect(screen.queryByText("chat:retryStatus.backoff.waitingWithAttemptMax")).not.toBeInTheDocument()
})
it("updates countdown when remainingSeconds changes", () => {
const metadata1: RetryStatusMetadata = {
type: "retry_status",
status: "waiting",
remainingSeconds: 10,
attempt: 1,
maxAttempts: 3,
origin: "retry_attempt",
cause: "rate_limit",
}
const { rerender } = render(<RetryStatusRow metadata={metadata1} />)
// Initial countdown
expect(screen.getByText("chat:retryStatus.rateLimit.waitingWithAttemptMax")).toBeInTheDocument()
// Update countdown
const metadata2: RetryStatusMetadata = {
...metadata1,
remainingSeconds: 5,
}
rerender(<RetryStatusRow metadata={metadata2} />)
// Should still show the same text key but with updated seconds
expect(screen.getByText("chat:retryStatus.rateLimit.waitingWithAttemptMax")).toBeInTheDocument()
})
})

View file

@ -143,6 +143,14 @@
"cancelled": "Sol·licitud API cancel·lada",
"streamingFailed": "Transmissió API ha fallat"
},
"retryStatus": {
"title": "Sol·licitud endarrerida — si us plau, espera.",
"waiting": "Reintentant en {{seconds}}s",
"waitingWithAttempt": "Reintentant en {{seconds}}s (intent {{attempt}})",
"waitingWithAttemptMax": "Reintentant en {{seconds}}s (intent {{attempt}}/{{maxAttempts}})",
"retrying": "Reintentant ara…",
"cancelled": "Reintent cancel·lat"
},
"rateLimitRetry": {
"title": "Límit de peticions assolit — si us plau, espera.",
"waiting": "Reintentant en {{seconds}}s",

View file

@ -143,6 +143,14 @@
"cancelled": "API-Anfrage abgebrochen",
"streamingFailed": "API-Streaming fehlgeschlagen"
},
"retryStatus": {
"title": "Anfrage verzögert — bitte warten.",
"waiting": "Wiederholung in {{seconds}}s",
"waitingWithAttempt": "Wiederholung in {{seconds}}s (Versuch {{attempt}})",
"waitingWithAttemptMax": "Wiederholung in {{seconds}}s (Versuch {{attempt}}/{{maxAttempts}})",
"retrying": "Wird wiederholt…",
"cancelled": "Wiederholung abgebrochen"
},
"rateLimitRetry": {
"title": "Ratenlimit ausgelöst — bitte warten.",
"waiting": "Wiederholung in {{seconds}}s",

View file

@ -149,6 +149,22 @@
"cancelled": "API Request Cancelled",
"streamingFailed": "API Streaming Failed"
},
"retryStatus": {
"rateLimit": {
"title": "Rate limit set to {{rateLimitSeconds}} seconds",
"waiting": "Waiting {{seconds}} seconds",
"proceeding": "Proceeding now…",
"cancelled": "Request cancelled"
},
"backoff": {
"title": "Request failed",
"waiting": "Trying in {{seconds}} seconds",
"waitingWithAttempt": "Trying in {{seconds}} seconds (attempt #{{attempt}})",
"waitingWithAttemptMax": "Trying in {{seconds}} seconds (attempt #{{attempt}}/{{maxAttempts}})",
"retrying": "Trying now…",
"cancelled": "Retry cancelled"
}
},
"rateLimitRetry": {
"title": "Rate limit triggered — please wait.",
"waiting": "Retrying in {{seconds}}s",

View file

@ -162,6 +162,24 @@
},
"current": "Actual"
},
"retryStatus": {
"rateLimit": {
"title": "Retraso por límite de velocidad — por favor, espera.",
"waiting": "Continuando en {{seconds}}s",
"waitingWithAttempt": "Continuando en {{seconds}}s (intento {{attempt}})",
"waitingWithAttemptMax": "Continuando en {{seconds}}s (intento {{attempt}}/{{maxAttempts}})",
"proceeding": "Continuando ahora…",
"cancelled": "Solicitud cancelada"
},
"backoff": {
"title": "Solicitud retrasada — reintentando.",
"waiting": "Reintentando en {{seconds}}s",
"waitingWithAttempt": "Reintentando en {{seconds}}s (intento {{attempt}})",
"waitingWithAttemptMax": "Reintentando en {{seconds}}s (intento {{attempt}}/{{maxAttempts}})",
"retrying": "Reintentando ahora…",
"cancelled": "Reintento cancelado"
}
},
"rateLimitRetry": {
"title": "Límite de tasa activado — por favor, espera.",
"waiting": "Reintentando en {{seconds}}s",

View file

@ -162,6 +162,14 @@
},
"current": "Actuel"
},
"retryStatus": {
"title": "Requête retardée — veuillez patienter.",
"waiting": "Nouvelle tentative dans {{seconds}}s",
"waitingWithAttempt": "Nouvelle tentative dans {{seconds}}s (tentative {{attempt}})",
"waitingWithAttemptMax": "Nouvelle tentative dans {{seconds}}s (tentative {{attempt}}/{{maxAttempts}})",
"retrying": "Nouvelle tentative en cours…",
"cancelled": "Nouvelle tentative annulée"
},
"rateLimitRetry": {
"title": "Limite de débit atteinte — veuillez patienter.",
"waiting": "Nouvel essai dans {{seconds}}s",

View file

@ -143,6 +143,14 @@
"cancelled": "API अनुरोध रद्द किया गया",
"streamingFailed": "API स्ट्रीमिंग विफल हुई"
},
"retryStatus": {
"title": "अनुरोध में देरी — कृपया प्रतीक्षा करें।",
"waiting": "{{seconds}}s में पुनः प्रयास",
"waitingWithAttempt": "{{seconds}}s में पुनः प्रयास (प्रयास {{attempt}})",
"waitingWithAttemptMax": "{{seconds}}s में पुनः प्रयास (प्रयास {{attempt}}/{{maxAttempts}})",
"retrying": "अभी पुनः प्रयास कर रहे हैं…",
"cancelled": "पुनः प्रयास रद्द"
},
"rateLimitRetry": {
"title": "दर सीमा ट्रिगर हुई — कृपया प्रतीक्षा करें।",
"waiting": "{{seconds}}s में पुनः प्रयास कर रहा है",

View file

@ -152,6 +152,14 @@
"cancelled": "Permintaan API Dibatalkan",
"streamingFailed": "Streaming API Gagal"
},
"retryStatus": {
"title": "Permintaan tertunda — mohon tunggu.",
"waiting": "Mencoba lagi dalam {{seconds}}s",
"waitingWithAttempt": "Mencoba lagi dalam {{seconds}}s (percobaan {{attempt}})",
"waitingWithAttemptMax": "Mencoba lagi dalam {{seconds}}s (percobaan {{attempt}}/{{maxAttempts}})",
"retrying": "Mencoba sekarang…",
"cancelled": "Percobaan ulang dibatalkan"
},
"rateLimitRetry": {
"title": "Batas kecepatan tercapai — mohon tunggu.",
"waiting": "Mencoba lagi dalam {{seconds}}dtk",

View file

@ -146,6 +146,14 @@
"cancelled": "Richiesta API annullata",
"streamingFailed": "Streaming API fallito"
},
"retryStatus": {
"title": "Richiesta ritardata — attendi.",
"waiting": "Riprovando tra {{seconds}}s",
"waitingWithAttempt": "Riprovando tra {{seconds}}s (tentativo {{attempt}})",
"waitingWithAttemptMax": "Riprovando tra {{seconds}}s (tentativo {{attempt}}/{{maxAttempts}})",
"retrying": "Riprovando ora…",
"cancelled": "Tentativo cancellato"
},
"rateLimitRetry": {
"title": "Limite di frequenza raggiunto — attendi.",
"waiting": "Riprovo tra {{seconds}}s",

View file

@ -143,6 +143,14 @@
"cancelled": "APIリクエストキャンセル",
"streamingFailed": "APIストリーミング失敗"
},
"retryStatus": {
"title": "リクエストが遅延しています — しばらくお待ちください。",
"waiting": "{{seconds}}秒後に再試行",
"waitingWithAttempt": "{{seconds}}秒後に再試行({{attempt}}回目の試行)",
"waitingWithAttemptMax": "{{seconds}}秒後に再試行({{attempt}}/{{maxAttempts}}回目の試行)",
"retrying": "再試行中…",
"cancelled": "再試行がキャンセルされました"
},
"rateLimitRetry": {
"title": "レート制限がトリガーされました — しばらくお待ちください。",
"waiting": "{{seconds}}秒後に再試行",

View file

@ -143,6 +143,14 @@
"cancelled": "API 요청 취소됨",
"streamingFailed": "API 스트리밍 실패"
},
"retryStatus": {
"title": "요청이 지연되었습니다 — 잠시 기다려주세요.",
"waiting": "{{seconds}}초 후 재시도",
"waitingWithAttempt": "{{seconds}}초 후 재시도 (시도 {{attempt}})",
"waitingWithAttemptMax": "{{seconds}}초 후 재시도 (시도 {{attempt}}/{{maxAttempts}})",
"retrying": "재시도 중…",
"cancelled": "재시도 취소됨"
},
"rateLimitRetry": {
"title": "속도 제한이 트리거되었습니다 — 잠시 기다려주세요.",
"waiting": "{{seconds}}초 후에 다시 시도",

View file

@ -138,6 +138,14 @@
"cancelled": "API-verzoek geannuleerd",
"streamingFailed": "API-streaming mislukt"
},
"retryStatus": {
"title": "Verzoek vertraagd — even geduld a.u.b.",
"waiting": "Opnieuw proberen over {{seconds}}s",
"waitingWithAttempt": "Opnieuw proberen over {{seconds}}s (poging {{attempt}})",
"waitingWithAttemptMax": "Opnieuw proberen over {{seconds}}s (poging {{attempt}}/{{maxAttempts}})",
"retrying": "Nu opnieuw proberen…",
"cancelled": "Herpoging geannuleerd"
},
"rateLimitRetry": {
"title": "Snelheidslimiet bereikt — even geduld a.u.b.",
"waiting": "Opnieuw proberen over {{seconds}}s",

View file

@ -143,6 +143,14 @@
"cancelled": "Zapytanie API anulowane",
"streamingFailed": "Strumieniowanie API nie powiodło się"
},
"retryStatus": {
"title": "Żądanie opóźnione — proszę czekać.",
"waiting": "Ponowna próba za {{seconds}}s",
"waitingWithAttempt": "Ponowna próba za {{seconds}}s (próba {{attempt}})",
"waitingWithAttemptMax": "Ponowna próba za {{seconds}}s (próba {{attempt}}/{{maxAttempts}})",
"retrying": "Ponawiam teraz…",
"cancelled": "Ponowna próba anulowana"
},
"rateLimitRetry": {
"title": "Osiągnięto limit szybkości — proszę czekać.",
"waiting": "Ponawianie za {{seconds}}s",

View file

@ -143,6 +143,14 @@
"cancelled": "Requisição API cancelada",
"streamingFailed": "Streaming API falhou"
},
"retryStatus": {
"title": "Solicitação atrasada — por favor, aguarde.",
"waiting": "Tentando novamente em {{seconds}}s",
"waitingWithAttempt": "Tentando novamente em {{seconds}}s (tentativa {{attempt}})",
"waitingWithAttemptMax": "Tentando novamente em {{seconds}}s (tentativa {{attempt}}/{{maxAttempts}})",
"retrying": "Tentando agora…",
"cancelled": "Tentativa cancelada"
},
"rateLimitRetry": {
"title": "Limite de taxa atingido — por favor, aguarde.",
"waiting": "Tentando novamente em {{seconds}}s",

View file

@ -138,6 +138,14 @@
"cancelled": "API-запрос отменен",
"streamingFailed": "Ошибка потокового API-запроса"
},
"retryStatus": {
"title": "Запрос задержан — пожалуйста, подождите.",
"waiting": "Повтор через {{seconds}}с",
"waitingWithAttempt": "Повтор через {{seconds}}с (попытка {{attempt}})",
"waitingWithAttemptMax": "Повтор через {{seconds}}с (попытка {{attempt}}/{{maxAttempts}})",
"retrying": "Повторяем…",
"cancelled": "Повтор отменен"
},
"rateLimitRetry": {
"title": "Превышен лимит запросов — пожалуйста, подождите.",
"waiting": "Повторная попытка через {{seconds}}с",

View file

@ -162,6 +162,14 @@
},
"current": "Mevcut"
},
"retryStatus": {
"title": "İstek gecikti — lütfen bekleyin.",
"waiting": "{{seconds}}s içinde yeniden deneniyor",
"waitingWithAttempt": "{{seconds}}s içinde yeniden deneniyor (deneme {{attempt}})",
"waitingWithAttemptMax": "{{seconds}}s içinde yeniden deneniyor (deneme {{attempt}}/{{maxAttempts}})",
"retrying": "Şimdi yeniden deneniyor…",
"cancelled": "Yeniden deneme iptal edildi"
},
"rateLimitRetry": {
"title": "Hız limiti tetiklendi — lütfen bekleyin.",
"waiting": "{{seconds}}s içinde tekrar deniyor",

View file

@ -143,6 +143,14 @@
"cancelled": "Yêu cầu API đã hủy",
"streamingFailed": "Streaming API thất bại"
},
"retryStatus": {
"title": "Yêu cầu bị trì hoãn — vui lòng đợi.",
"waiting": "Thử lại sau {{seconds}}s",
"waitingWithAttempt": "Thử lại sau {{seconds}}s (lần thử {{attempt}})",
"waitingWithAttemptMax": "Thử lại sau {{seconds}}s (lần thử {{attempt}}/{{maxAttempts}})",
"retrying": "Đang thử lại…",
"cancelled": "Thử lại đã bị hủy"
},
"rateLimitRetry": {
"title": "Đã đạt giới hạn tốc độ — vui lòng đợi.",
"waiting": "Đang thử lại sau {{seconds}}s",

View file

@ -143,6 +143,24 @@
"cancelled": "API请求已取消",
"streamingFailed": "API流式传输失败"
},
"retryStatus": {
"rateLimit": {
"title": "速率限制延迟 — 请稍候。",
"waiting": "{{seconds}}秒后继续",
"waitingWithAttempt": "{{seconds}}秒后继续(第 {{attempt}} 次尝试)",
"waitingWithAttemptMax": "{{seconds}}秒后继续(第 {{attempt}}/{{maxAttempts}} 次尝试)",
"proceeding": "正在继续…",
"cancelled": "请求已取消"
},
"backoff": {
"title": "请求已延迟 — 重试中。",
"waiting": "{{seconds}}秒后重试",
"waitingWithAttempt": "{{seconds}}秒后重试(第 {{attempt}} 次尝试)",
"waitingWithAttemptMax": "{{seconds}}秒后重试(第 {{attempt}}/{{maxAttempts}} 次尝试)",
"retrying": "正在重试…",
"cancelled": "重试已取消"
}
},
"rateLimitRetry": {
"title": "API 请求频率限制已触发 — 请稍候。",
"waiting": "正在 {{seconds}} 秒后重试",

View file

@ -149,6 +149,14 @@
"cancelled": "API 請求已取消",
"streamingFailed": "API 串流處理失敗"
},
"retryStatus": {
"title": "請求已延遲 — 請稍候。",
"waiting": "{{seconds}}秒後重試",
"waitingWithAttempt": "{{seconds}}秒後重試(第 {{attempt}} 次嘗試)",
"waitingWithAttemptMax": "{{seconds}}秒後重試(第 {{attempt}}/{{maxAttempts}} 次嘗試)",
"retrying": "正在重試…",
"cancelled": "重試已取消"
},
"rateLimitRetry": {
"title": "已觸發速率限制 — 請稍候。",
"waiting": "正在 {{seconds}} 秒後重試",