Skip to content

Commit 53683c6

Browse files
committed
feat(config): migrate rate limit settings to profile-specific configurations
This commit introduces a migration function that transfers the global rate limit setting to individual API profile configurations. The `migrateRateLimitToProfiles` method updates each configuration with the global rate limit value and removes the global setting afterward. Additionally, the handling of rate limit values has been updated across various components to ensure consistency. - Added default rate limit to new configurations. - Updated ClineProvider to manage rate limit values from the current API configuration. - Adjusted tests to verify the migration functionality and ensure proper handling of rate limits.
1 parent 1b37a71 commit 53683c6

File tree

9 files changed

+207
-17
lines changed

9 files changed

+207
-17
lines changed

src/core/Cline.ts

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

1073-
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
1073+
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
10741074
(await this.providerRef.deref()?.getState()) ?? {}
10751075

10761076
let rateLimitDelay = 0
@@ -1079,7 +1079,8 @@ export class Cline extends EventEmitter<ClineEvents> {
10791079
if (this.lastApiRequestTime) {
10801080
const now = Date.now()
10811081
const timeSinceLastRequest = now - this.lastApiRequestTime
1082-
const rateLimit = rateLimitSeconds || 0
1082+
// Get rate limit from the current API configuration instead of global state
1083+
const rateLimit = this.apiConfiguration.rateLimitSeconds || 0
10831084
rateLimitDelay = Math.ceil(Math.max(0, rateLimit * 1000 - timeSinceLastRequest) / 1000)
10841085
}
10851086

src/core/config/ConfigManager.ts

Lines changed: 53 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,40 @@ 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 = this.context.globalState
55+
? (await this.context.globalState.get<number>("rateLimitSeconds")) || 0
56+
: 0
57+
58+
// Get all configurations
59+
const config = await this.readConfig()
60+
61+
// Update each configuration with the global rate limit
62+
for (const apiConfig of Object.values(config.apiConfigs)) {
63+
apiConfig.rateLimitSeconds = globalRateLimit
64+
}
65+
66+
// Save the updated configurations
67+
await this.writeConfig(config)
68+
69+
// Remove the global rate limit setting
70+
if (this.context.globalState) {
71+
await this.context.globalState.update("rateLimitSeconds", undefined)
72+
}
73+
74+
console.log(`[ConfigManager] Migrated global rate limit (${globalRateLimit}s) to all profiles`)
75+
})
76+
} catch (error) {
77+
throw new Error(`Failed to migrate rate limit settings: ${error}`)
78+
}
79+
}
80+
4681
async initConfig(): Promise<void> {
4782
try {
4883
return await this.lock(async () => {
@@ -52,18 +87,27 @@ export class ConfigManager {
5287
return
5388
}
5489

55-
// Migrate: ensure all configs have IDs
56-
let needsMigration = false
90+
// Check if migration is needed for IDs
91+
let needsIdMigration = false
5792
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
5893
if (!apiConfig.id) {
5994
apiConfig.id = this.generateId()
60-
needsMigration = true
95+
needsIdMigration = true
6196
}
6297
}
6398

64-
if (needsMigration) {
99+
if (needsIdMigration) {
65100
await this.writeConfig(config)
66101
}
102+
103+
// Check if rate limit migration is needed
104+
if (this.context.globalState) {
105+
const hasGlobalRateLimit =
106+
(await this.context.globalState.get<number>("rateLimitSeconds")) !== undefined
107+
if (hasGlobalRateLimit) {
108+
await this.migrateRateLimitToProfiles()
109+
}
110+
}
67111
})
68112
} catch (error) {
69113
throw new Error(`Failed to initialize config: ${error}`)
@@ -99,6 +143,11 @@ export class ConfigManager {
99143
currentConfig.apiConfigs[name] = {
100144
...config,
101145
id: existingConfig?.id || this.generateId(),
146+
// Preserve rateLimitSeconds if not explicitly set in the new config
147+
rateLimitSeconds:
148+
config.rateLimitSeconds !== undefined
149+
? config.rateLimitSeconds
150+
: existingConfig?.rateLimitSeconds || 0,
102151
}
103152
await this.writeConfig(currentConfig)
104153
})

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

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ describe("ConfigManager", () => {
173173
test: {
174174
...newConfig,
175175
id: testConfigId,
176+
rateLimitSeconds: 0, // Default rate limit is 0 seconds
176177
},
177178
},
178179
modeApiConfigs: {
@@ -216,6 +217,7 @@ describe("ConfigManager", () => {
216217
apiProvider: "anthropic",
217218
apiKey: "new-key",
218219
id: "test-id",
220+
rateLimitSeconds: 0, // Default rate limit is 0 seconds
219221
},
220222
},
221223
}
@@ -489,4 +491,133 @@ describe("ConfigManager", () => {
489491
)
490492
})
491493
})
494+
495+
describe("migrateRateLimitToProfiles", () => {
496+
it("should migrate global rate limit to all profiles", async () => {
497+
// Setup mock context with global rate limit
498+
const mockGlobalState = {
499+
get: jest.fn().mockResolvedValue(15), // Mock global rate limit of 15 seconds
500+
update: jest.fn(),
501+
}
502+
;(mockContext as any).globalState = mockGlobalState
503+
504+
// Setup existing configs without rate limits
505+
const existingConfig: ApiConfigData = {
506+
currentApiConfigName: "default",
507+
apiConfigs: {
508+
default: {
509+
id: "default",
510+
apiProvider: "anthropic",
511+
},
512+
test: {
513+
id: "test-id",
514+
apiProvider: "openrouter",
515+
},
516+
},
517+
}
518+
519+
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
520+
521+
// Call the migration method
522+
await configManager.migrateRateLimitToProfiles()
523+
524+
// Verify the configs were updated with the rate limit
525+
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
526+
expect(storedConfig.apiConfigs.default.rateLimitSeconds).toBe(15)
527+
expect(storedConfig.apiConfigs.test.rateLimitSeconds).toBe(15)
528+
529+
// Verify the global rate limit was removed
530+
expect(mockGlobalState.update).toHaveBeenCalledWith("rateLimitSeconds", undefined)
531+
})
532+
533+
it("should handle empty config case", async () => {
534+
// Create a new instance with fresh mocks for this test
535+
jest.resetAllMocks()
536+
537+
const testMockContext = { ...mockContext }
538+
const testMockSecrets = {
539+
get: jest.fn(),
540+
store: jest.fn(),
541+
delete: jest.fn(),
542+
onDidChange: jest.fn(),
543+
}
544+
testMockContext.secrets = testMockSecrets
545+
546+
// Setup mock context with global rate limit
547+
const testMockGlobalState = {
548+
get: jest.fn().mockResolvedValue(10), // Mock global rate limit of 10 seconds
549+
update: jest.fn().mockResolvedValue(undefined),
550+
keys: jest.fn().mockReturnValue([]),
551+
setKeysForSync: jest.fn(),
552+
}
553+
testMockContext.globalState = testMockGlobalState
554+
555+
// Setup empty config
556+
const emptyConfig: ApiConfigData = {
557+
currentApiConfigName: "default",
558+
apiConfigs: {},
559+
}
560+
561+
// Mock the readConfig and writeConfig methods
562+
testMockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
563+
testMockSecrets.store.mockResolvedValue(undefined)
564+
565+
// Create a test instance that won't be affected by other tests
566+
const testConfigManager = new ConfigManager(testMockContext as any)
567+
568+
// Override the lock method for this test
569+
testConfigManager["_lock"] = Promise.resolve()
570+
const originalLock = testConfigManager["lock"]
571+
testConfigManager["lock"] = function (cb: () => Promise<any>) {
572+
return cb()
573+
}
574+
575+
// Call the migration method
576+
await testConfigManager.migrateRateLimitToProfiles()
577+
578+
// Verify the global rate limit was removed even with empty config
579+
expect(testMockGlobalState.update).toHaveBeenCalledWith("rateLimitSeconds", undefined)
580+
})
581+
582+
it("should handle errors gracefully", async () => {
583+
// Create a new instance with fresh mocks for this test
584+
jest.resetAllMocks()
585+
586+
const testMockContext = { ...mockContext }
587+
const testMockSecrets = {
588+
get: jest.fn(),
589+
store: jest.fn(),
590+
delete: jest.fn(),
591+
onDidChange: jest.fn(),
592+
}
593+
testMockContext.secrets = testMockSecrets
594+
595+
// Setup mock context with global rate limit
596+
const testMockGlobalState = {
597+
get: jest.fn().mockResolvedValue(5), // Mock global rate limit of 5 seconds
598+
update: jest.fn().mockResolvedValue(undefined),
599+
keys: jest.fn().mockReturnValue([]),
600+
setKeysForSync: jest.fn(),
601+
}
602+
testMockContext.globalState = testMockGlobalState
603+
604+
// Force an error during read
605+
testMockSecrets.get.mockRejectedValue(new Error("Storage failed"))
606+
607+
// Create a test instance that won't be affected by other tests
608+
const testConfigManager = new ConfigManager(testMockContext as any)
609+
610+
// Override the lock method for this test
611+
testConfigManager["_lock"] = Promise.resolve()
612+
const originalLock = testConfigManager["lock"]
613+
testConfigManager["lock"] = function (cb: () => Promise<any>) {
614+
return Promise.reject(new Error("Failed to read config from secrets: Error: Storage failed"))
615+
}
616+
617+
// Expect the migration to throw an error
618+
await expect(testConfigManager.migrateRateLimitToProfiles()).rejects.toThrow(
619+
"Failed to migrate rate limit settings: Error: Failed to read config from secrets: Error: Storage failed",
620+
)
621+
})
622+
})
492623
})

src/core/webview/ClineProvider.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,7 +1366,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
13661366
await this.postStateToWebview()
13671367
break
13681368
case "rateLimitSeconds":
1369-
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
1369+
// Update the current API configuration with the rate limit value
1370+
const currentApiConfigName = (await this.getGlobalState("currentApiConfigName")) as string
1371+
if (currentApiConfigName) {
1372+
const apiConfig = await this.configManager.loadConfig(currentApiConfigName)
1373+
apiConfig.rateLimitSeconds = message.value ?? 0
1374+
await this.configManager.saveConfig(currentApiConfigName, apiConfig)
1375+
await this.updateApiConfiguration(apiConfig)
1376+
}
13701377
await this.postStateToWebview()
13711378
break
13721379
case "writeDelayMs":
@@ -2331,7 +2338,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
23312338
enableMcpServerCreation,
23322339
alwaysApproveResubmit,
23332340
requestDelaySeconds,
2334-
rateLimitSeconds,
2341+
// rateLimitSeconds is now part of apiConfiguration
23352342
currentApiConfigName,
23362343
listApiConfigMeta,
23372344
mode,
@@ -2392,7 +2399,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
23922399
enableMcpServerCreation: enableMcpServerCreation ?? true,
23932400
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
23942401
requestDelaySeconds: requestDelaySeconds ?? 10,
2395-
rateLimitSeconds: rateLimitSeconds ?? 0,
2402+
rateLimitSeconds: apiConfiguration.rateLimitSeconds ?? 0,
23962403
currentApiConfigName: currentApiConfigName ?? "default",
23972404
listApiConfigMeta: listApiConfigMeta ?? [],
23982405
mode: mode ?? defaultModeSlug,
@@ -2550,7 +2557,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
25502557
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
25512558
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
25522559
requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
2553-
rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
2560+
// rateLimitSeconds is now part of the API configuration
25542561
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
25552562
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
25562563
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
@@ -132,6 +133,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
132133
"modelTemperature",
133134
"modelMaxTokens",
134135
"modelMaxThinkingTokens",
136+
"rateLimitSeconds", // Added for profile-specific rate limiting
135137
]
136138

137139
// 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)