Skip to content

Commit e8f0b35

Browse files
authored
Merge pull request RooVetGit#688 from RooVetGit/rate_limiting
Add optional rate limiting between API calls
2 parents 9acc21c + 9aeb498 commit e8f0b35

File tree

9 files changed

+80
-3
lines changed

9 files changed

+80
-3
lines changed

src/core/Cline.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class Cline {
9696
didFinishAborting = false
9797
abandoned = false
9898
private diffViewProvider: DiffViewProvider
99+
private lastApiRequestTime?: number
99100

100101
// streaming
101102
private currentStreamingContentIndex = 0
@@ -796,9 +797,40 @@ export class Cline {
796797
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
797798
let mcpHub: McpHub | undefined
798799

799-
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
800+
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
800801
(await this.providerRef.deref()?.getState()) ?? {}
801802

803+
let finalDelay = 0
804+
805+
// Only apply rate limiting if this isn't the first request
806+
if (this.lastApiRequestTime) {
807+
const now = Date.now()
808+
const timeSinceLastRequest = now - this.lastApiRequestTime
809+
const rateLimit = rateLimitSeconds || 0
810+
const rateLimitDelay = Math.max(0, rateLimit * 1000 - timeSinceLastRequest)
811+
finalDelay = rateLimitDelay
812+
}
813+
814+
// Add exponential backoff delay for retries
815+
if (retryAttempt > 0) {
816+
const baseDelay = requestDelaySeconds || 5
817+
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) * 1000
818+
finalDelay = Math.max(finalDelay, exponentialDelay)
819+
}
820+
821+
if (finalDelay > 0) {
822+
// Show countdown timer
823+
for (let i = Math.ceil(finalDelay / 1000); i > 0; i--) {
824+
const delayMessage =
825+
retryAttempt > 0 ? `Retrying in ${i} seconds...` : `Rate limiting for ${i} seconds...`
826+
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
827+
await delay(1000)
828+
}
829+
}
830+
831+
// Update last request time before making the request
832+
this.lastApiRequestTime = Date.now()
833+
802834
if (mcpEnabled ?? true) {
803835
mcpHub = this.providerRef.deref()?.mcpHub
804836
if (!mcpHub) {

src/core/__tests__/Cline.test.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,11 @@ describe("Cline", () => {
750750
false,
751751
)
752752

753-
// Verify delay was called correctly
754-
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
753+
// Calculate expected delay calls based on exponential backoff
754+
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, 1)) // retryAttempt = 1
755+
const rateLimitDelay = baseDelay // Initial rate limit delay
756+
const totalExpectedDelays = exponentialDelay + rateLimitDelay
757+
expect(mockDelay).toHaveBeenCalledTimes(totalExpectedDelays)
755758
expect(mockDelay).toHaveBeenCalledWith(1000)
756759

757760
// Verify error message content

src/core/webview/ClineProvider.ts

+10
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ type GlobalStateKey =
112112
| "mcpEnabled"
113113
| "alwaysApproveResubmit"
114114
| "requestDelaySeconds"
115+
| "rateLimitSeconds"
115116
| "currentApiConfigName"
116117
| "listApiConfigMeta"
117118
| "vsCodeLmModelSelector"
@@ -886,6 +887,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
886887
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
887888
await this.postStateToWebview()
888889
break
890+
case "rateLimitSeconds":
891+
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
892+
await this.postStateToWebview()
893+
break
889894
case "preferredLanguage":
890895
await this.updateGlobalState("preferredLanguage", message.text)
891896
await this.postStateToWebview()
@@ -1997,6 +2002,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19972002
mcpEnabled,
19982003
alwaysApproveResubmit,
19992004
requestDelaySeconds,
2005+
rateLimitSeconds,
20002006
currentApiConfigName,
20012007
listApiConfigMeta,
20022008
mode,
@@ -2038,6 +2044,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20382044
mcpEnabled: mcpEnabled ?? true,
20392045
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
20402046
requestDelaySeconds: requestDelaySeconds ?? 10,
2047+
rateLimitSeconds: rateLimitSeconds ?? 0,
20412048
currentApiConfigName: currentApiConfigName ?? "default",
20422049
listApiConfigMeta: listApiConfigMeta ?? [],
20432050
mode: mode ?? defaultModeSlug,
@@ -2161,6 +2168,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21612168
mcpEnabled,
21622169
alwaysApproveResubmit,
21632170
requestDelaySeconds,
2171+
rateLimitSeconds,
21642172
currentApiConfigName,
21652173
listApiConfigMeta,
21662174
vsCodeLmModelSelector,
@@ -2233,6 +2241,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22332241
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
22342242
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
22352243
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
2244+
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
22362245
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
22372246
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
22382247
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
@@ -2355,6 +2364,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23552364
mcpEnabled: mcpEnabled ?? true,
23562365
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
23572366
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
2367+
rateLimitSeconds: rateLimitSeconds ?? 0,
23582368
currentApiConfigName: currentApiConfigName ?? "default",
23592369
listApiConfigMeta: listApiConfigMeta ?? [],
23602370
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),

src/core/webview/__tests__/ClineProvider.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ describe("ClineProvider", () => {
324324
fuzzyMatchThreshold: 1.0,
325325
mcpEnabled: true,
326326
requestDelaySeconds: 5,
327+
rateLimitSeconds: 0,
327328
mode: defaultModeSlug,
328329
customModes: [],
329330
experiments: experimentDefault,

src/shared/ExtensionMessage.ts

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface ExtensionState {
9494
alwaysApproveResubmit?: boolean
9595
alwaysAllowModeSwitch?: boolean
9696
requestDelaySeconds: number
97+
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
9798
uriScheme?: string
9899
allowedCommands?: string[]
99100
soundEnabled?: boolean

src/shared/WebviewMessage.ts

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface WebviewMessage {
6666
| "refreshGlamaModels"
6767
| "alwaysApproveResubmit"
6868
| "requestDelaySeconds"
69+
| "rateLimitSeconds"
6970
| "setApiConfigPassword"
7071
| "requestVsCodeLmModels"
7172
| "mode"

webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("AutoApproveMenu", () => {
2828
terminalOutputLineLimit: 500,
2929
mcpEnabled: true,
3030
requestDelaySeconds: 5,
31+
rateLimitSeconds: 0,
3132
currentApiConfigName: "default",
3233
listApiConfigMeta: [],
3334
mode: defaultModeSlug,
@@ -78,6 +79,7 @@ describe("AutoApproveMenu", () => {
7879
setMcpEnabled: jest.fn(),
7980
setAlwaysApproveResubmit: jest.fn(),
8081
setRequestDelaySeconds: jest.fn(),
82+
setRateLimitSeconds: jest.fn(),
8183
setCurrentApiConfigName: jest.fn(),
8284
setListApiConfigMeta: jest.fn(),
8385
onUpdateApiConfig: jest.fn(),

webview-ui/src/components/settings/SettingsView.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
5353
setAlwaysApproveResubmit,
5454
requestDelaySeconds,
5555
setRequestDelaySeconds,
56+
rateLimitSeconds,
57+
setRateLimitSeconds,
5658
currentApiConfigName,
5759
listApiConfigMeta,
5860
experiments,
@@ -92,6 +94,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
9294
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
9395
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
9496
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
97+
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
9598
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
9699
vscode.postMessage({
97100
type: "upsertApiConfiguration",
@@ -572,6 +575,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
572575

573576
<div style={{ marginBottom: 40 }}>
574577
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
578+
<div style={{ marginBottom: 15 }}>
579+
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
580+
<span style={{ fontWeight: "500" }}>Rate limit</span>
581+
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
582+
<input
583+
type="range"
584+
min="0"
585+
max="60"
586+
step="1"
587+
value={rateLimitSeconds}
588+
onChange={(e) => setRateLimitSeconds(parseInt(e.target.value))}
589+
style={{ ...sliderStyle }}
590+
/>
591+
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
592+
</div>
593+
</div>
594+
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
595+
Minimum time between API requests.
596+
</p>
597+
</div>
575598
<div style={{ marginBottom: 15 }}>
576599
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
577600
<span style={{ fontWeight: "500" }}>Terminal output limit</span>

webview-ui/src/context/ExtensionStateContext.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export interface ExtensionStateContextType extends ExtensionState {
5555
setAlwaysApproveResubmit: (value: boolean) => void
5656
requestDelaySeconds: number
5757
setRequestDelaySeconds: (value: number) => void
58+
rateLimitSeconds: number
59+
setRateLimitSeconds: (value: number) => void
5860
setCurrentApiConfigName: (value: string) => void
5961
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
6062
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
@@ -92,6 +94,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
9294
mcpEnabled: true,
9395
alwaysApproveResubmit: false,
9496
requestDelaySeconds: 5,
97+
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
9598
currentApiConfigName: "default",
9699
listApiConfigMeta: [],
97100
mode: defaultModeSlug,
@@ -271,6 +274,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
271274
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
272275
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
273276
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
277+
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
274278
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
275279
setListApiConfigMeta,
276280
onUpdateApiConfig,

0 commit comments

Comments
 (0)