Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,16 +1059,33 @@ export class Cline extends EventEmitter<ClineEvents> {
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
let mcpHub: McpHub | undefined

const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
(await this.providerRef.deref()?.getState()) ?? {}
const {
mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
currentApiConfigName,
profileSpecificSettings,
} = (await this.providerRef.deref()?.getState()) ?? {}

let rateLimitDelay = 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

// Check if there's a profile-specific rate limit for the current profile
let rateLimit = rateLimitSeconds || 0
if (
currentApiConfigName &&
profileSpecificSettings &&
profileSpecificSettings[currentApiConfigName] &&
profileSpecificSettings[currentApiConfigName].rateLimitSeconds !== undefined
) {
rateLimit = profileSpecificSettings[currentApiConfigName].rateLimitSeconds
}

rateLimitDelay = Math.ceil(Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000)
}

Expand Down
117 changes: 117 additions & 0 deletions src/core/__tests__/Cline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,123 @@ describe("Cline", () => {
await task.catch(() => {})
})
})

describe("rate limiting", () => {
it("should use profile-specific rate limit when available", async () => {
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

// Set the lastApiRequestTime using Object.defineProperty to bypass private access
Object.defineProperty(cline, "lastApiRequestTime", {
value: Date.now() - 2000, // 2 seconds ago
writable: true,
})

// Mock the provider's getState to return profile-specific settings
mockProvider.getState = jest.fn().mockResolvedValue({
rateLimitSeconds: 5, // Global rate limit of 5 seconds
currentApiConfigName: "test-profile", // Current profile
profileSpecificSettings: {
"test-profile": {
rateLimitSeconds: 10, // Profile-specific rate limit of 10 seconds
},
},
})

// Mock say to track rate limit delay messages
const saySpy = jest.spyOn(cline, "say")

// Create a successful stream for the API request with the correct type
const mockSuccessStream = (async function* () {
yield { type: "text" as const, text: "Success" }
})()

// Mock createMessage to return the success stream
jest.spyOn(cline.api, "createMessage").mockReturnValue(mockSuccessStream as any)

// Mock delay to track countdown timing
const mockDelay = jest.fn().mockResolvedValue(undefined)
jest.spyOn(require("delay"), "default").mockImplementation(mockDelay)

// Trigger API request
const iterator = cline.attemptApiRequest(0)
await iterator.next()

// Verify that the profile-specific rate limit was used (10 seconds)
// With 2 seconds elapsed, we should have an 8-second delay
expect(saySpy).toHaveBeenCalledWith(
"api_req_retry_delayed",
expect.stringContaining("Rate limiting for 8 seconds"),
undefined,
true,
)

// Clean up
await cline.abortTask(true)
await task.catch(() => {})
})

it("should use global rate limit when no profile-specific setting exists", async () => {
const [cline, task] = Cline.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
})

// Set the lastApiRequestTime using Object.defineProperty to bypass private access
Object.defineProperty(cline, "lastApiRequestTime", {
value: Date.now() - 2000, // 2 seconds ago
writable: true,
})

// Mock the provider's getState to return only global settings
mockProvider.getState = jest.fn().mockResolvedValue({
rateLimitSeconds: 5, // Global rate limit of 5 seconds
currentApiConfigName: "test-profile", // Current profile
profileSpecificSettings: {
// No settings for test-profile
"other-profile": {
rateLimitSeconds: 10,
},
},
})

// Mock say to track rate limit delay messages
const saySpy = jest.spyOn(cline, "say")

// Create a successful stream for the API request with the correct type
const mockSuccessStream = (async function* () {
yield { type: "text" as const, text: "Success" }
})()

// Mock createMessage to return the success stream
jest.spyOn(cline.api, "createMessage").mockReturnValue(mockSuccessStream as any)

// Mock delay to track countdown timing
const mockDelay = jest.fn().mockResolvedValue(undefined)
jest.spyOn(require("delay"), "default").mockImplementation(mockDelay)

// Trigger API request
const iterator = cline.attemptApiRequest(0)
await iterator.next()

// Verify that the global rate limit was used (5 seconds)
// With 2 seconds elapsed, we should have a 3-second delay
expect(saySpy).toHaveBeenCalledWith(
"api_req_retry_delayed",
expect.stringContaining("Rate limiting for 3 seconds"),
undefined,
true,
)

// Clean up
await cline.abortTask(true)
await task.catch(() => {})
})
})
})
})
})
7 changes: 7 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("enableCustomModeCreation", message.bool ?? true)
await this.postStateToWebview()
break
case "profileSpecificSettings":
await this.updateGlobalState("profileSpecificSettings", message.values)
await this.postStateToWebview()
break
case "autoApprovalEnabled":
await this.updateGlobalState("autoApprovalEnabled", message.bool ?? false)
await this.postStateToWebview()
Expand Down Expand Up @@ -2275,6 +2279,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowMcp,
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
profileSpecificSettings,
soundEnabled,
diffEnabled,
enableCheckpoints,
Expand Down Expand Up @@ -2373,6 +2378,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
language,
profileSpecificSettings: profileSpecificSettings || {},
}
}

Expand Down Expand Up @@ -2528,6 +2534,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserToolEnabled: stateValues.browserToolEnabled ?? true,
telemetrySetting: stateValues.telemetrySetting || "unset",
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
profileSpecificSettings: stateValues.profileSpecificSettings || {},
}
}

Expand Down
1 change: 1 addition & 0 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export type GlobalStateKey =
| "telemetrySetting"
| "showRooIgnoredFiles"
| "remoteBrowserEnabled"
| "profileSpecificSettings"

export type ConfigurationKey = GlobalStateKey | SecretKey

Expand Down
7 changes: 7 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ export interface ExtensionState {
telemetryKey?: string
machineId?: string
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
profileSpecificSettings?: {
[profileId: string]: {
rateLimitSeconds?: number
diffEnabled?: boolean
fuzzyMatchThreshold?: number
}
} // Profile-specific settings that override global settings
}

export type { ClineMessage, ClineAsk, ClineSay }
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface WebviewMessage {
| "discoverBrowser"
| "browserConnectionResult"
| "remoteBrowserEnabled"
| "profileSpecificSettings"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
Expand Down
1 change: 1 addition & 0 deletions src/shared/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const GLOBAL_STATE_KEYS = [
"showRooIgnoredFiles",
"remoteBrowserEnabled",
"maxWorkspaceFiles",
"profileSpecificSettings", // Profile-specific settings that override global settings
] as const

type CheckGlobalStateKeysExhaustiveness =
Expand Down
Loading
Loading