mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge pull request #688 from RooVetGit/rate_limiting
Add optional rate limiting between API calls
This commit is contained in:
@@ -96,6 +96,7 @@ export class Cline {
|
|||||||
didFinishAborting = false
|
didFinishAborting = false
|
||||||
abandoned = false
|
abandoned = false
|
||||||
private diffViewProvider: DiffViewProvider
|
private diffViewProvider: DiffViewProvider
|
||||||
|
private lastApiRequestTime?: number
|
||||||
|
|
||||||
// streaming
|
// streaming
|
||||||
private currentStreamingContentIndex = 0
|
private currentStreamingContentIndex = 0
|
||||||
@@ -796,9 +797,40 @@ export class Cline {
|
|||||||
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
|
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
|
||||||
let mcpHub: McpHub | undefined
|
let mcpHub: McpHub | undefined
|
||||||
|
|
||||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
|
||||||
(await this.providerRef.deref()?.getState()) ?? {}
|
(await this.providerRef.deref()?.getState()) ?? {}
|
||||||
|
|
||||||
|
let finalDelay = 0
|
||||||
|
|
||||||
|
// Only apply rate limiting if this isn't the first request
|
||||||
|
if (this.lastApiRequestTime) {
|
||||||
|
const now = Date.now()
|
||||||
|
const timeSinceLastRequest = now - this.lastApiRequestTime
|
||||||
|
const rateLimit = rateLimitSeconds || 0
|
||||||
|
const rateLimitDelay = Math.max(0, rateLimit * 1000 - timeSinceLastRequest)
|
||||||
|
finalDelay = rateLimitDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add exponential backoff delay for retries
|
||||||
|
if (retryAttempt > 0) {
|
||||||
|
const baseDelay = requestDelaySeconds || 5
|
||||||
|
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) * 1000
|
||||||
|
finalDelay = Math.max(finalDelay, exponentialDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDelay > 0) {
|
||||||
|
// Show countdown timer
|
||||||
|
for (let i = Math.ceil(finalDelay / 1000); i > 0; i--) {
|
||||||
|
const delayMessage =
|
||||||
|
retryAttempt > 0 ? `Retrying in ${i} seconds...` : `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
|
||||||
|
this.lastApiRequestTime = Date.now()
|
||||||
|
|
||||||
if (mcpEnabled ?? true) {
|
if (mcpEnabled ?? true) {
|
||||||
mcpHub = this.providerRef.deref()?.mcpHub
|
mcpHub = this.providerRef.deref()?.mcpHub
|
||||||
if (!mcpHub) {
|
if (!mcpHub) {
|
||||||
|
|||||||
@@ -750,8 +750,11 @@ describe("Cline", () => {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify delay was called correctly
|
// Calculate expected delay calls based on exponential backoff
|
||||||
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
|
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, 1)) // retryAttempt = 1
|
||||||
|
const rateLimitDelay = baseDelay // Initial rate limit delay
|
||||||
|
const totalExpectedDelays = exponentialDelay + rateLimitDelay
|
||||||
|
expect(mockDelay).toHaveBeenCalledTimes(totalExpectedDelays)
|
||||||
expect(mockDelay).toHaveBeenCalledWith(1000)
|
expect(mockDelay).toHaveBeenCalledWith(1000)
|
||||||
|
|
||||||
// Verify error message content
|
// Verify error message content
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type GlobalStateKey =
|
|||||||
| "mcpEnabled"
|
| "mcpEnabled"
|
||||||
| "alwaysApproveResubmit"
|
| "alwaysApproveResubmit"
|
||||||
| "requestDelaySeconds"
|
| "requestDelaySeconds"
|
||||||
|
| "rateLimitSeconds"
|
||||||
| "currentApiConfigName"
|
| "currentApiConfigName"
|
||||||
| "listApiConfigMeta"
|
| "listApiConfigMeta"
|
||||||
| "vsCodeLmModelSelector"
|
| "vsCodeLmModelSelector"
|
||||||
@@ -886,6 +887,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
|
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
|
case "rateLimitSeconds":
|
||||||
|
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
case "preferredLanguage":
|
case "preferredLanguage":
|
||||||
await this.updateGlobalState("preferredLanguage", message.text)
|
await this.updateGlobalState("preferredLanguage", message.text)
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
@@ -1997,6 +2002,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
alwaysApproveResubmit,
|
alwaysApproveResubmit,
|
||||||
requestDelaySeconds,
|
requestDelaySeconds,
|
||||||
|
rateLimitSeconds,
|
||||||
currentApiConfigName,
|
currentApiConfigName,
|
||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
mode,
|
mode,
|
||||||
@@ -2038,6 +2044,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled: mcpEnabled ?? true,
|
mcpEnabled: mcpEnabled ?? true,
|
||||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||||
requestDelaySeconds: requestDelaySeconds ?? 10,
|
requestDelaySeconds: requestDelaySeconds ?? 10,
|
||||||
|
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||||
currentApiConfigName: currentApiConfigName ?? "default",
|
currentApiConfigName: currentApiConfigName ?? "default",
|
||||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||||
mode: mode ?? defaultModeSlug,
|
mode: mode ?? defaultModeSlug,
|
||||||
@@ -2161,6 +2168,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
alwaysApproveResubmit,
|
alwaysApproveResubmit,
|
||||||
requestDelaySeconds,
|
requestDelaySeconds,
|
||||||
|
rateLimitSeconds,
|
||||||
currentApiConfigName,
|
currentApiConfigName,
|
||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
vsCodeLmModelSelector,
|
vsCodeLmModelSelector,
|
||||||
@@ -2233,6 +2241,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
|
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
||||||
|
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
|
||||||
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
||||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||||
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||||
@@ -2355,6 +2364,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled: mcpEnabled ?? true,
|
mcpEnabled: mcpEnabled ?? true,
|
||||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||||
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
|
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
|
||||||
|
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||||
currentApiConfigName: currentApiConfigName ?? "default",
|
currentApiConfigName: currentApiConfigName ?? "default",
|
||||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||||
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
|
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ describe("ClineProvider", () => {
|
|||||||
fuzzyMatchThreshold: 1.0,
|
fuzzyMatchThreshold: 1.0,
|
||||||
mcpEnabled: true,
|
mcpEnabled: true,
|
||||||
requestDelaySeconds: 5,
|
requestDelaySeconds: 5,
|
||||||
|
rateLimitSeconds: 0,
|
||||||
mode: defaultModeSlug,
|
mode: defaultModeSlug,
|
||||||
customModes: [],
|
customModes: [],
|
||||||
experiments: experimentDefault,
|
experiments: experimentDefault,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export interface ExtensionState {
|
|||||||
alwaysApproveResubmit?: boolean
|
alwaysApproveResubmit?: boolean
|
||||||
alwaysAllowModeSwitch?: boolean
|
alwaysAllowModeSwitch?: boolean
|
||||||
requestDelaySeconds: number
|
requestDelaySeconds: number
|
||||||
|
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
|
||||||
uriScheme?: string
|
uriScheme?: string
|
||||||
allowedCommands?: string[]
|
allowedCommands?: string[]
|
||||||
soundEnabled?: boolean
|
soundEnabled?: boolean
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export interface WebviewMessage {
|
|||||||
| "refreshGlamaModels"
|
| "refreshGlamaModels"
|
||||||
| "alwaysApproveResubmit"
|
| "alwaysApproveResubmit"
|
||||||
| "requestDelaySeconds"
|
| "requestDelaySeconds"
|
||||||
|
| "rateLimitSeconds"
|
||||||
| "setApiConfigPassword"
|
| "setApiConfigPassword"
|
||||||
| "requestVsCodeLmModels"
|
| "requestVsCodeLmModels"
|
||||||
| "mode"
|
| "mode"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe("AutoApproveMenu", () => {
|
|||||||
terminalOutputLineLimit: 500,
|
terminalOutputLineLimit: 500,
|
||||||
mcpEnabled: true,
|
mcpEnabled: true,
|
||||||
requestDelaySeconds: 5,
|
requestDelaySeconds: 5,
|
||||||
|
rateLimitSeconds: 0,
|
||||||
currentApiConfigName: "default",
|
currentApiConfigName: "default",
|
||||||
listApiConfigMeta: [],
|
listApiConfigMeta: [],
|
||||||
mode: defaultModeSlug,
|
mode: defaultModeSlug,
|
||||||
@@ -78,6 +79,7 @@ describe("AutoApproveMenu", () => {
|
|||||||
setMcpEnabled: jest.fn(),
|
setMcpEnabled: jest.fn(),
|
||||||
setAlwaysApproveResubmit: jest.fn(),
|
setAlwaysApproveResubmit: jest.fn(),
|
||||||
setRequestDelaySeconds: jest.fn(),
|
setRequestDelaySeconds: jest.fn(),
|
||||||
|
setRateLimitSeconds: jest.fn(),
|
||||||
setCurrentApiConfigName: jest.fn(),
|
setCurrentApiConfigName: jest.fn(),
|
||||||
setListApiConfigMeta: jest.fn(),
|
setListApiConfigMeta: jest.fn(),
|
||||||
onUpdateApiConfig: jest.fn(),
|
onUpdateApiConfig: jest.fn(),
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setAlwaysApproveResubmit,
|
setAlwaysApproveResubmit,
|
||||||
requestDelaySeconds,
|
requestDelaySeconds,
|
||||||
setRequestDelaySeconds,
|
setRequestDelaySeconds,
|
||||||
|
rateLimitSeconds,
|
||||||
|
setRateLimitSeconds,
|
||||||
currentApiConfigName,
|
currentApiConfigName,
|
||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
experiments,
|
experiments,
|
||||||
@@ -92,6 +94,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
|
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
|
||||||
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
|
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
|
||||||
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
|
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
|
||||||
|
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
|
||||||
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
|
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "upsertApiConfiguration",
|
type: "upsertApiConfiguration",
|
||||||
@@ -572,6 +575,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
|
|
||||||
<div style={{ marginBottom: 40 }}>
|
<div style={{ marginBottom: 40 }}>
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
|
||||||
|
<div style={{ marginBottom: 15 }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Rate limit</span>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="60"
|
||||||
|
step="1"
|
||||||
|
value={rateLimitSeconds}
|
||||||
|
onChange={(e) => setRateLimitSeconds(parseInt(e.target.value))}
|
||||||
|
style={{ ...sliderStyle }}
|
||||||
|
/>
|
||||||
|
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||||
|
Minimum time between API requests.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div style={{ marginBottom: 15 }}>
|
<div style={{ marginBottom: 15 }}>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||||
<span style={{ fontWeight: "500" }}>Terminal output limit</span>
|
<span style={{ fontWeight: "500" }}>Terminal output limit</span>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setAlwaysApproveResubmit: (value: boolean) => void
|
setAlwaysApproveResubmit: (value: boolean) => void
|
||||||
requestDelaySeconds: number
|
requestDelaySeconds: number
|
||||||
setRequestDelaySeconds: (value: number) => void
|
setRequestDelaySeconds: (value: number) => void
|
||||||
|
rateLimitSeconds: number
|
||||||
|
setRateLimitSeconds: (value: number) => void
|
||||||
setCurrentApiConfigName: (value: string) => void
|
setCurrentApiConfigName: (value: string) => void
|
||||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||||
@@ -92,6 +94,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
mcpEnabled: true,
|
mcpEnabled: true,
|
||||||
alwaysApproveResubmit: false,
|
alwaysApproveResubmit: false,
|
||||||
requestDelaySeconds: 5,
|
requestDelaySeconds: 5,
|
||||||
|
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
|
||||||
currentApiConfigName: "default",
|
currentApiConfigName: "default",
|
||||||
listApiConfigMeta: [],
|
listApiConfigMeta: [],
|
||||||
mode: defaultModeSlug,
|
mode: defaultModeSlug,
|
||||||
@@ -271,6 +274,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
|
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
|
||||||
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
|
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
|
||||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||||
|
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
|
||||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||||
setListApiConfigMeta,
|
setListApiConfigMeta,
|
||||||
onUpdateApiConfig,
|
onUpdateApiConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user