mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Fix rate limit wait display (#10389)
Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
parent
c37aa02b21
commit
ca1bc18a21
25 changed files with 218 additions and 24 deletions
|
|
@ -301,6 +301,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
"diff_error",
|
||||
"condense_context",
|
||||
"condense_context_error",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_retried",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
|
|||
* - `api_req_finished`: Indicates an API request has completed successfully
|
||||
* - `api_req_retried`: Indicates an API request is being retried after a failure
|
||||
* - `api_req_retry_delayed`: Indicates an API request retry has been delayed
|
||||
* - `api_req_rate_limit_wait`: Indicates a configured rate-limit wait (not an error)
|
||||
* - `api_req_deleted`: Indicates an API request has been deleted/cancelled
|
||||
* - `text`: General text message or assistant response
|
||||
* - `reasoning`: Assistant's reasoning or thought process (often hidden from user)
|
||||
|
|
@ -155,6 +156,7 @@ export const clineSays = [
|
|||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_deleted",
|
||||
"text",
|
||||
"image",
|
||||
|
|
|
|||
|
|
@ -2347,6 +2347,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
const modelId = getModelId(this.apiConfiguration)
|
||||
const apiProtocol = getApiProtocol(this.apiConfiguration.apiProvider, modelId)
|
||||
|
||||
// Respect user-configured provider rate limiting BEFORE we emit api_req_started.
|
||||
// This prevents the UI from showing an "API Request..." spinner while we are
|
||||
// intentionally waiting due to the rate limit slider.
|
||||
//
|
||||
// NOTE: We also set Task.lastGlobalApiRequestTime here to reserve this slot
|
||||
// before we build environment details (which can take time).
|
||||
// This ensures subsequent requests (including subtasks) still honour the
|
||||
// provider rate-limit window.
|
||||
await this.maybeWaitForProviderRateLimit(currentItem.retryAttempt ?? 0)
|
||||
Task.lastGlobalApiRequestTime = performance.now()
|
||||
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
|
|
@ -2554,7 +2565,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Yields only if the first chunk is successful, otherwise will
|
||||
// allow the user to retry the request (most likely due to rate
|
||||
// limit error, which gets thrown on the first chunk).
|
||||
const stream = this.attemptApiRequest()
|
||||
const stream = this.attemptApiRequest(currentItem.retryAttempt ?? 0, { skipProviderRateLimit: true })
|
||||
let assistantMessage = ""
|
||||
let reasoningMessage = ""
|
||||
let pendingGroundingSources: GroundingSource[] = []
|
||||
|
|
@ -3656,7 +3667,44 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
await this.providerRef.deref()?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
|
||||
}
|
||||
|
||||
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
|
||||
/**
|
||||
* Enforce the user-configured provider rate limit.
|
||||
*
|
||||
* NOTE: This is intentionally treated as expected behavior and is surfaced via
|
||||
* the `api_req_rate_limit_wait` say type (not an error).
|
||||
*/
|
||||
private async maybeWaitForProviderRateLimit(retryAttempt: number): Promise<void> {
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const rateLimitSeconds =
|
||||
state?.apiConfiguration?.rateLimitSeconds ?? this.apiConfiguration?.rateLimitSeconds ?? 0
|
||||
|
||||
if (rateLimitSeconds <= 0 || !Task.lastGlobalApiRequestTime) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = performance.now()
|
||||
const timeSinceLastRequest = now - Task.lastGlobalApiRequestTime
|
||||
const rateLimitDelay = Math.ceil(
|
||||
Math.min(rateLimitSeconds, Math.max(0, rateLimitSeconds * 1000 - timeSinceLastRequest) / 1000),
|
||||
)
|
||||
|
||||
// Only show the countdown UX on the first attempt. Retry flows have their own delay messaging.
|
||||
if (rateLimitDelay > 0 && retryAttempt === 0) {
|
||||
for (let i = rateLimitDelay; i > 0; i--) {
|
||||
// Send structured JSON data for i18n-safe transport
|
||||
const delayMessage = JSON.stringify({ seconds: i })
|
||||
await this.say("api_req_rate_limit_wait", delayMessage, undefined, true)
|
||||
await delay(1000)
|
||||
}
|
||||
// Finalize the partial message so the UI doesn't keep rendering an in-progress spinner.
|
||||
await this.say("api_req_rate_limit_wait", undefined, undefined, false)
|
||||
}
|
||||
}
|
||||
|
||||
public async *attemptApiRequest(
|
||||
retryAttempt: number = 0,
|
||||
options: { skipProviderRateLimit?: boolean } = {},
|
||||
): ApiStream {
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
|
||||
const {
|
||||
|
|
@ -3693,29 +3741,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
}
|
||||
}
|
||||
|
||||
let rateLimitDelay = 0
|
||||
|
||||
// Use the shared timestamp so that subtasks respect the same rate-limit
|
||||
// window as their parent tasks.
|
||||
if (Task.lastGlobalApiRequestTime) {
|
||||
const now = performance.now()
|
||||
const timeSinceLastRequest = now - Task.lastGlobalApiRequestTime
|
||||
const rateLimit = apiConfiguration?.rateLimitSeconds || 0
|
||||
rateLimitDelay = Math.ceil(Math.min(rateLimit, Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000))
|
||||
if (!options.skipProviderRateLimit) {
|
||||
await this.maybeWaitForProviderRateLimit(retryAttempt)
|
||||
}
|
||||
|
||||
// Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there.
|
||||
if (rateLimitDelay > 0 && retryAttempt === 0) {
|
||||
// Show countdown timer
|
||||
for (let i = rateLimitDelay; i > 0; i--) {
|
||||
const delayMessage = `Rate limiting for ${i} seconds...`
|
||||
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
|
||||
await delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
// Update last request time before making the request so that subsequent
|
||||
// Update last request time right before making the request so that subsequent
|
||||
// requests — even from new subtasks — will honour the provider's rate-limit.
|
||||
//
|
||||
// NOTE: When recursivelyMakeClineRequests handles rate limiting, it sets the
|
||||
// timestamp earlier to include the environment details build. We still set it
|
||||
// here for direct callers (tests) and for the case where we didn't rate-limit
|
||||
// in the caller.
|
||||
Task.lastGlobalApiRequestTime = performance.now()
|
||||
|
||||
const systemPrompt = await this.getSystemPrompt()
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,9 @@ describe("Cline", () => {
|
|||
startTask: false,
|
||||
})
|
||||
|
||||
// Spy on child.say to verify the emitted message type
|
||||
const saySpy = vi.spyOn(child, "say")
|
||||
|
||||
// Mock the child's API stream
|
||||
const childMockStream = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
|
|
@ -1067,6 +1070,17 @@ describe("Cline", () => {
|
|||
// Verify rate limiting was applied
|
||||
expect(mockDelay).toHaveBeenCalledTimes(mockApiConfig.rateLimitSeconds)
|
||||
expect(mockDelay).toHaveBeenCalledWith(1000)
|
||||
|
||||
// Verify we used the non-error rate-limit wait message type (JSON format)
|
||||
expect(saySpy).toHaveBeenCalledWith(
|
||||
"api_req_rate_limit_wait",
|
||||
expect.stringMatching(/\{"seconds":\d+\}/),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
|
||||
// Verify the wait message was finalized
|
||||
expect(saySpy).toHaveBeenCalledWith("api_req_rate_limit_wait", undefined, undefined, false)
|
||||
}, 10000) // Increase timeout to 10 seconds
|
||||
|
||||
it("should not apply rate limiting if enough time has passed", async () => {
|
||||
|
|
|
|||
|
|
@ -298,6 +298,8 @@ export const ChatRowContent = ({
|
|||
style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span style={{ color: successColor, fontWeight: "bold" }}>{t("chat:taskCompleted")}</span>,
|
||||
]
|
||||
case "api_req_rate_limit_wait":
|
||||
return []
|
||||
case "api_req_retry_delayed":
|
||||
return []
|
||||
case "api_req_started":
|
||||
|
|
@ -327,8 +329,10 @@ export const ChatRowContent = ({
|
|||
getIconSpan("arrow-swap", normalColor)
|
||||
) : apiRequestFailedMessage ? (
|
||||
getIconSpan("error", errorColor)
|
||||
) : (
|
||||
) : isLast ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
getIconSpan("arrow-swap", normalColor)
|
||||
),
|
||||
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
|
|
@ -356,7 +360,17 @@ export const ChatRowContent = ({
|
|||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage, t])
|
||||
}, [
|
||||
type,
|
||||
isCommandExecuting,
|
||||
message,
|
||||
isMcpServerResponding,
|
||||
apiReqCancelReason,
|
||||
cost,
|
||||
apiRequestFailedMessage,
|
||||
t,
|
||||
isLast,
|
||||
])
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
|
|
@ -1149,6 +1163,35 @@ export const ChatRowContent = ({
|
|||
errorDetails={rawError}
|
||||
/>
|
||||
)
|
||||
case "api_req_rate_limit_wait": {
|
||||
const isWaiting = message.partial === true
|
||||
|
||||
const waitSeconds = (() => {
|
||||
if (!message.text) return undefined
|
||||
try {
|
||||
const data = JSON.parse(message.text)
|
||||
return typeof data.seconds === "number" ? data.seconds : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
|
||||
return isWaiting && waitSeconds !== undefined ? (
|
||||
<div
|
||||
className={`group text-sm transition-opacity opacity-100`}
|
||||
style={{
|
||||
...headerStyle,
|
||||
marginBottom: 0,
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
||||
<ProgressIndicator />
|
||||
<span style={{ color: normalColor }}>{t("chat:apiRequest.rateLimitWait")}</span>
|
||||
</div>
|
||||
<span className="text-xs font-light text-vscode-descriptionForeground">{waitSeconds}s</span>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
case "api_req_finished":
|
||||
return null // we should never see this message type
|
||||
case "text":
|
||||
|
|
|
|||
|
|
@ -400,6 +400,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// an "ask" while ask is waiting for response.
|
||||
switch (lastMessage.say) {
|
||||
case "api_req_retry_delayed":
|
||||
case "api_req_rate_limit_wait":
|
||||
setSendingDisabled(true)
|
||||
break
|
||||
case "api_req_started":
|
||||
|
|
@ -957,6 +958,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "api_req_deleted":
|
||||
return false
|
||||
case "api_req_retry_delayed":
|
||||
case "api_req_rate_limit_wait":
|
||||
const last1 = modifiedMessages.at(-1)
|
||||
const last2 = modifiedMessages.at(-2)
|
||||
if (last1?.ask === "resume_task" && last2 === message) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import React from "react"
|
||||
|
||||
import { render, screen } from "@/utils/test-utils"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
|
||||
import { ChatRowContent } from "../ChatRow"
|
||||
|
||||
// Mock i18n
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const map: Record<string, string> = {
|
||||
"chat:apiRequest.rateLimitWait": "Rate limiting",
|
||||
}
|
||||
return map[key] ?? key
|
||||
},
|
||||
}),
|
||||
Trans: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
||||
initReactI18next: { type: "3rdParty", init: () => {} },
|
||||
}))
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
function renderChatRow(message: any) {
|
||||
return render(
|
||||
<ExtensionStateContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatRowContent
|
||||
message={message}
|
||||
isExpanded={false}
|
||||
isLast={false}
|
||||
isStreaming={false}
|
||||
onToggleExpand={() => {}}
|
||||
onSuggestionClick={() => {}}
|
||||
onBatchFileResponse={() => {}}
|
||||
onFollowUpUnmount={() => {}}
|
||||
isFollowUpAnswered={false}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("ChatRow - rate limit wait", () => {
|
||||
it("renders a non-error progress row for api_req_rate_limit_wait", () => {
|
||||
const message: any = {
|
||||
type: "say",
|
||||
say: "api_req_rate_limit_wait",
|
||||
ts: Date.now(),
|
||||
partial: true,
|
||||
text: JSON.stringify({ seconds: 1 }),
|
||||
}
|
||||
|
||||
renderChatRow(message)
|
||||
|
||||
expect(screen.getByText("Rate limiting")).toBeInTheDocument()
|
||||
// Should show countdown, but should NOT show the error-details affordance.
|
||||
expect(screen.getByText("1s")).toBeInTheDocument()
|
||||
expect(screen.queryByText("Details")).toBeNull()
|
||||
})
|
||||
|
||||
it("renders nothing when rate limit wait is complete", () => {
|
||||
const message: any = {
|
||||
type: "say",
|
||||
say: "api_req_rate_limit_wait",
|
||||
ts: Date.now(),
|
||||
partial: false,
|
||||
text: undefined,
|
||||
}
|
||||
|
||||
const { container } = renderChatRow(message)
|
||||
|
||||
// The row should be hidden when rate limiting is complete
|
||||
expect(screen.queryByText("Rate limiting")).toBeNull()
|
||||
// Nothing should be rendered
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
1
webview-ui/src/i18n/locales/ca/chat.json
generated
1
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Sol·licitud API...",
|
||||
"cancelled": "Sol·licitud API cancel·lada",
|
||||
"streamingFailed": "Transmissió API ha fallat",
|
||||
"rateLimitWait": "Limitació de taxa",
|
||||
"errorTitle": "Error de proveïdor {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentació",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/de/chat.json
generated
1
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "API-Anfrage...",
|
||||
"cancelled": "API-Anfrage abgebrochen",
|
||||
"streamingFailed": "API-Streaming fehlgeschlagen",
|
||||
"rateLimitWait": "Ratenbegrenzung",
|
||||
"errorTitle": "Anbieter-Fehler {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Dokumentation",
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@
|
|||
"streaming": "API Request...",
|
||||
"cancelled": "API Request Cancelled",
|
||||
"streamingFailed": "API Streaming Failed",
|
||||
"rateLimitWait": "Rate limiting",
|
||||
"errorTitle": "Provider Error {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Docs",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/es/chat.json
generated
1
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Solicitud API...",
|
||||
"cancelled": "Solicitud API cancelada",
|
||||
"streamingFailed": "Transmisión API falló",
|
||||
"rateLimitWait": "Limitación de tasa",
|
||||
"errorTitle": "Error del proveedor {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentación",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/fr/chat.json
generated
1
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Requête API...",
|
||||
"cancelled": "Requête API annulée",
|
||||
"streamingFailed": "Échec du streaming API",
|
||||
"rateLimitWait": "Limitation du débit",
|
||||
"errorTitle": "Erreur du fournisseur {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentation",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/hi/chat.json
generated
1
webview-ui/src/i18n/locales/hi/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "API अनुरोध...",
|
||||
"cancelled": "API अनुरोध रद्द किया गया",
|
||||
"streamingFailed": "API स्ट्रीमिंग विफल हुई",
|
||||
"rateLimitWait": "दर सीमा",
|
||||
"errorTitle": "प्रदाता त्रुटि {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "डॉक्स",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/id/chat.json
generated
1
webview-ui/src/i18n/locales/id/chat.json
generated
|
|
@ -147,6 +147,7 @@
|
|||
"streaming": "Permintaan API...",
|
||||
"cancelled": "Permintaan API Dibatalkan",
|
||||
"streamingFailed": "Streaming API Gagal",
|
||||
"rateLimitWait": "Pembatasan rate",
|
||||
"errorTitle": "Kesalahan Penyedia {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Dokumentasi",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/it/chat.json
generated
1
webview-ui/src/i18n/locales/it/chat.json
generated
|
|
@ -141,6 +141,7 @@
|
|||
"streaming": "Richiesta API...",
|
||||
"cancelled": "Richiesta API annullata",
|
||||
"streamingFailed": "Streaming API fallito",
|
||||
"rateLimitWait": "Limitazione della frequenza",
|
||||
"errorTitle": "Errore del fornitore {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentazione",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/ja/chat.json
generated
1
webview-ui/src/i18n/locales/ja/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "APIリクエスト...",
|
||||
"cancelled": "APIリクエストキャンセル",
|
||||
"streamingFailed": "APIストリーミング失敗",
|
||||
"rateLimitWait": "レート制限中",
|
||||
"errorTitle": "プロバイダーエラー {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "ドキュメント",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/ko/chat.json
generated
1
webview-ui/src/i18n/locales/ko/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "API 요청...",
|
||||
"cancelled": "API 요청 취소됨",
|
||||
"streamingFailed": "API 스트리밍 실패",
|
||||
"rateLimitWait": "속도 제한",
|
||||
"errorTitle": "공급자 오류 {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "문서",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/nl/chat.json
generated
1
webview-ui/src/i18n/locales/nl/chat.json
generated
|
|
@ -133,6 +133,7 @@
|
|||
"streaming": "API-verzoek...",
|
||||
"cancelled": "API-verzoek geannuleerd",
|
||||
"streamingFailed": "API-streaming mislukt",
|
||||
"rateLimitWait": "Snelheidsbeperking",
|
||||
"errorTitle": "Fout van provider {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentatie",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/pl/chat.json
generated
1
webview-ui/src/i18n/locales/pl/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Zapytanie API...",
|
||||
"cancelled": "Zapytanie API anulowane",
|
||||
"streamingFailed": "Strumieniowanie API nie powiodło się",
|
||||
"rateLimitWait": "Ograniczenie szybkości",
|
||||
"errorTitle": "Błąd dostawcy {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Dokumentacja",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
1
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Requisição API...",
|
||||
"cancelled": "Requisição API cancelada",
|
||||
"streamingFailed": "Streaming API falhou",
|
||||
"rateLimitWait": "Limitação de taxa",
|
||||
"errorTitle": "Erro do provedor {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Documentação",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/ru/chat.json
generated
1
webview-ui/src/i18n/locales/ru/chat.json
generated
|
|
@ -133,6 +133,7 @@
|
|||
"streaming": "API-запрос...",
|
||||
"cancelled": "API-запрос отменен",
|
||||
"streamingFailed": "Ошибка потокового API-запроса",
|
||||
"rateLimitWait": "Ограничение частоты",
|
||||
"errorTitle": "Ошибка провайдера {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Документация",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/tr/chat.json
generated
1
webview-ui/src/i18n/locales/tr/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "API İsteği...",
|
||||
"cancelled": "API İsteği İptal Edildi",
|
||||
"streamingFailed": "API Akışı Başarısız",
|
||||
"rateLimitWait": "Hız sınırlaması",
|
||||
"errorTitle": "Sağlayıcı Hatası {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Belgeler",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/vi/chat.json
generated
1
webview-ui/src/i18n/locales/vi/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "Yêu cầu API...",
|
||||
"cancelled": "Yêu cầu API đã hủy",
|
||||
"streamingFailed": "Streaming API thất bại",
|
||||
"rateLimitWait": "Giới hạn tốc độ",
|
||||
"errorTitle": "Lỗi nhà cung cấp {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "Tài liệu",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
1
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -138,6 +138,7 @@
|
|||
"streaming": "API请求...",
|
||||
"cancelled": "API请求已取消",
|
||||
"streamingFailed": "API流式传输失败",
|
||||
"rateLimitWait": "请求频率限制",
|
||||
"errorTitle": "提供商错误 {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "文档",
|
||||
|
|
|
|||
1
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
1
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
|
|
@ -144,6 +144,7 @@
|
|||
"streaming": "正在處理 API 請求...",
|
||||
"cancelled": "API 請求已取消",
|
||||
"streamingFailed": "API 串流處理失敗",
|
||||
"rateLimitWait": "速率限制",
|
||||
"errorTitle": "提供商錯誤 {{code}}",
|
||||
"errorMessage": {
|
||||
"docs": "文件",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue