Skip to content

Commit b4a1506

Browse files
committed
feat(config): migrate global rate limit to profile-specific settings
This commit introduces a new method to migrate the global rate limit setting to individual API configurations. The `migrateRateLimitToProfiles` method updates all existing configurations with the global rate limit value and removes the global setting. Additionally, the rate limit is now part of the API configuration, allowing for more granular control. - Added migration logic in `ConfigManager`. - Updated `ClineProvider` to handle rate limit updates. - Adjusted related components to accommodate the new rate limit structure. Tests have been added to ensure the migration works correctly and handles edge cases.
1 parent e67ed9c commit b4a1506

File tree

9 files changed

+149
-17
lines changed

9 files changed

+149
-17
lines changed

src/core/Cline.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ export class Cline extends EventEmitter<ClineEvents> {
10591059
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
10601060
let mcpHub: McpHub | undefined
10611061

1062-
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
1062+
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
10631063
(await this.providerRef.deref()?.getState()) ?? {}
10641064

10651065
let rateLimitDelay = 0
@@ -1068,7 +1068,8 @@ export class Cline extends EventEmitter<ClineEvents> {
10681068
if (this.lastApiRequestTime) {
10691069
const now = Date.now()
10701070
const timeSinceLastRequest = now - this.lastApiRequestTime
1071-
const rateLimit = rateLimitSeconds || 0
1071+
// Get rate limit from the current API configuration instead of global state
1072+
const rateLimit = this.apiConfiguration.rateLimitSeconds || 0
10721073
rateLimitDelay = Math.ceil(Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000)
10731074
}
10741075

src/core/config/ConfigManager.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class ConfigManager {
1717
apiConfigs: {
1818
default: {
1919
id: this.generateId(),
20+
rateLimitSeconds: 0, // Default rate limit is 0 seconds
2021
},
2122
},
2223
}
@@ -43,6 +44,36 @@ export class ConfigManager {
4344
/**
4445
* Initialize config if it doesn't exist
4546
*/
47+
/**
48+
* Migrate rate limit from global state to profile-specific setting
49+
*/
50+
async migrateRateLimitToProfiles(): Promise<void> {
51+
try {
52+
return await this.lock(async () => {
53+
// Get the current global rate limit value
54+
const globalRateLimit = (await this.context.globalState.get<number>("rateLimitSeconds")) || 0
55+
56+
// Get all configurations
57+
const config = await this.readConfig()
58+
59+
// Update each configuration with the global rate limit
60+
for (const apiConfig of Object.values(config.apiConfigs)) {
61+
apiConfig.rateLimitSeconds = globalRateLimit
62+
}
63+
64+
// Save the updated configurations
65+
await this.writeConfig(config)
66+
67+
// Remove the global rate limit setting
68+
await this.context.globalState.update("rateLimitSeconds", undefined)
69+
70+
console.log(`[ConfigManager] Migrated global rate limit (${globalRateLimit}s) to all profiles`)
71+
})
72+
} catch (error) {
73+
throw new Error(`Failed to migrate rate limit settings: ${error}`)
74+
}
75+
}
76+
4677
async initConfig(): Promise<void> {
4778
try {
4879
return await this.lock(async () => {
@@ -52,18 +83,25 @@ export class ConfigManager {
5283
return
5384
}
5485

55-
// Migrate: ensure all configs have IDs
56-
let needsMigration = false
86+
// Check if migration is needed for IDs
87+
let needsIdMigration = false
5788
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
5889
if (!apiConfig.id) {
5990
apiConfig.id = this.generateId()
60-
needsMigration = true
91+
needsIdMigration = true
6192
}
6293
}
6394

64-
if (needsMigration) {
95+
if (needsIdMigration) {
6596
await this.writeConfig(config)
6697
}
98+
99+
// Check if rate limit migration is needed
100+
const hasGlobalRateLimit =
101+
(await this.context.globalState.get<number>("rateLimitSeconds")) !== undefined
102+
if (hasGlobalRateLimit) {
103+
await this.migrateRateLimitToProfiles()
104+
}
67105
})
68106
} catch (error) {
69107
throw new Error(`Failed to initialize config: ${error}`)
@@ -99,6 +137,11 @@ export class ConfigManager {
99137
currentConfig.apiConfigs[name] = {
100138
...config,
101139
id: existingConfig?.id || this.generateId(),
140+
// Preserve rateLimitSeconds if not explicitly set in the new config
141+
rateLimitSeconds:
142+
config.rateLimitSeconds !== undefined
143+
? config.rateLimitSeconds
144+
: existingConfig?.rateLimitSeconds || 0,
102145
}
103146
await this.writeConfig(currentConfig)
104147
})

src/core/config/__tests__/ConfigManager.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,83 @@ describe("ConfigManager", () => {
489489
)
490490
})
491491
})
492+
493+
describe("migrateRateLimitToProfiles", () => {
494+
it("should migrate global rate limit to all profiles", async () => {
495+
// Setup mock context with global rate limit
496+
const mockGlobalState = {
497+
get: jest.fn().mockResolvedValue(15), // Mock global rate limit of 15 seconds
498+
update: jest.fn(),
499+
}
500+
;(mockContext as any).globalState = mockGlobalState
501+
502+
// Setup existing configs without rate limits
503+
const existingConfig: ApiConfigData = {
504+
currentApiConfigName: "default",
505+
apiConfigs: {
506+
default: {
507+
id: "default",
508+
apiProvider: "anthropic",
509+
},
510+
test: {
511+
id: "test-id",
512+
apiProvider: "openrouter",
513+
},
514+
},
515+
}
516+
517+
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
518+
519+
// Call the migration method
520+
await configManager.migrateRateLimitToProfiles()
521+
522+
// Verify the configs were updated with the rate limit
523+
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
524+
expect(storedConfig.apiConfigs.default.rateLimitSeconds).toBe(15)
525+
expect(storedConfig.apiConfigs.test.rateLimitSeconds).toBe(15)
526+
527+
// Verify the global rate limit was removed
528+
expect(mockGlobalState.update).toHaveBeenCalledWith("rateLimitSeconds", undefined)
529+
})
530+
531+
it("should handle empty config case", async () => {
532+
// Setup mock context with global rate limit
533+
const mockGlobalState = {
534+
get: jest.fn().mockResolvedValue(10), // Mock global rate limit of 10 seconds
535+
update: jest.fn(),
536+
}
537+
;(mockContext as any).globalState = mockGlobalState
538+
539+
// Setup empty config
540+
const emptyConfig: ApiConfigData = {
541+
currentApiConfigName: "default",
542+
apiConfigs: {},
543+
}
544+
545+
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
546+
547+
// Call the migration method
548+
await configManager.migrateRateLimitToProfiles()
549+
550+
// Verify the global rate limit was removed even with empty config
551+
expect(mockGlobalState.update).toHaveBeenCalledWith("rateLimitSeconds", undefined)
552+
})
553+
554+
it("should handle errors gracefully", async () => {
555+
// Setup mock context with global rate limit
556+
const mockGlobalState = {
557+
get: jest.fn().mockResolvedValue(5), // Mock global rate limit of 5 seconds
558+
update: jest.fn(),
559+
}
560+
;(mockContext as any).globalState = mockGlobalState
561+
562+
// Force an error during read
563+
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
564+
565+
// Expect the migration to throw an error
566+
await expect(configManager.migrateRateLimitToProfiles()).rejects.toThrow(
567+
"Failed to migrate rate limit settings: Error: Failed to read config from secrets: Error: Storage failed",
568+
)
569+
})
570+
})
492571
})

src/core/webview/ClineProvider.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,7 +1331,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13311331
await this.postStateToWebview()
13321332
break
13331333
case "rateLimitSeconds":
1334-
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
1334+
// Update the current API configuration with the rate limit value
1335+
const currentApiConfigName = (await this.getGlobalState("currentApiConfigName")) as string
1336+
if (currentApiConfigName) {
1337+
const apiConfig = await this.configManager.loadConfig(currentApiConfigName)
1338+
apiConfig.rateLimitSeconds = message.value ?? 0
1339+
await this.configManager.saveConfig(currentApiConfigName, apiConfig)
1340+
await this.updateApiConfiguration(apiConfig)
1341+
}
13351342
await this.postStateToWebview()
13361343
break
13371344
case "writeDelayMs":
@@ -2296,7 +2303,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22962303
enableMcpServerCreation,
22972304
alwaysApproveResubmit,
22982305
requestDelaySeconds,
2299-
rateLimitSeconds,
2306+
// rateLimitSeconds is now part of apiConfiguration
23002307
currentApiConfigName,
23012308
listApiConfigMeta,
23022309
mode,
@@ -2357,7 +2364,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23572364
enableMcpServerCreation: enableMcpServerCreation ?? true,
23582365
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
23592366
requestDelaySeconds: requestDelaySeconds ?? 10,
2360-
rateLimitSeconds: rateLimitSeconds ?? 0,
2367+
rateLimitSeconds: apiConfiguration.rateLimitSeconds ?? 0,
23612368
currentApiConfigName: currentApiConfigName ?? "default",
23622369
listApiConfigMeta: listApiConfigMeta ?? [],
23632370
mode: mode ?? defaultModeSlug,
@@ -2515,7 +2522,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
25152522
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
25162523
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
25172524
requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
2518-
rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
2525+
// rateLimitSeconds is now part of the API configuration
25192526
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
25202527
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
25212528
modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export interface ExtensionState {
119119
alwaysAllowSubtasks?: boolean
120120
browserToolEnabled?: boolean
121121
requestDelaySeconds: number
122-
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
122+
rateLimitSeconds?: number // Minimum time between successive requests (0 = disabled)
123123
uriScheme?: string
124124
currentTaskItem?: HistoryItem
125125
allowedCommands?: string[]

src/shared/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export interface ApiHandlerOptions {
8080
export type ApiConfiguration = ApiHandlerOptions & {
8181
apiProvider?: ApiProvider
8282
id?: string // stable unique identifier
83+
rateLimitSeconds?: number // Rate limit in seconds between API requests
8384
}
8485

8586
// Import GlobalStateKey type from globalState.ts
@@ -130,6 +131,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
130131
"modelTemperature",
131132
"modelMaxTokens",
132133
"modelMaxThinkingTokens",
134+
"rateLimitSeconds", // Added for profile-specific rate limiting
133135
]
134136

135137
// Models

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { SectionHeader } from "./SectionHeader"
1313
import { Section } from "./Section"
1414

1515
type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
16-
rateLimitSeconds: number
16+
rateLimitSeconds?: number
1717
diffEnabled?: boolean
1818
fuzzyMatchThreshold?: number
1919
setCachedStateField: SetCachedStateField<"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold">
@@ -50,11 +50,11 @@ export const AdvancedSettings = ({
5050
min="0"
5151
max="60"
5252
step="1"
53-
value={rateLimitSeconds}
53+
value={rateLimitSeconds || 0}
5454
onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
5555
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
5656
/>
57-
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
57+
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds || 0}s</span>
5858
</div>
5959
</div>
6060
<p className="text-vscode-descriptionForeground text-sm mt-0">

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
199199
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
200200
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
201201
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
202-
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
202+
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds || 0 })
203203
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
204204
vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
205205
vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
@@ -440,7 +440,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
440440

441441
<div ref={advancedRef}>
442442
<AdvancedSettings
443-
rateLimitSeconds={rateLimitSeconds}
443+
rateLimitSeconds={rateLimitSeconds || 0}
444444
diffEnabled={diffEnabled}
445445
fuzzyMatchThreshold={fuzzyMatchThreshold}
446446
setCachedStateField={setCachedStateField}

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface ExtensionStateContextType extends ExtensionState {
5454
setAlwaysApproveResubmit: (value: boolean) => void
5555
requestDelaySeconds: number
5656
setRequestDelaySeconds: (value: number) => void
57-
rateLimitSeconds: number
57+
rateLimitSeconds?: number
5858
setRateLimitSeconds: (value: number) => void
5959
setCurrentApiConfigName: (value: string) => void
6060
setListApiConfigMeta: (value: ApiConfigMeta[]) => void

0 commit comments

Comments
 (0)