feat(settings): Rate-limit setting updated to be per-profile (#2376)

* Rate-limit setting updated to be per-profile

* Correct rateLimitSeconds translations

* Add missing d to rateLimitSecondsMigrate

* Fix fr rate-limit translation
This commit is contained in:
Ross McFarland 2025-04-07 13:24:38 -07:00 committed by GitHub
parent af9e471c92
commit 260fc30043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 275 additions and 95 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": minor
---
Rate-limit setting updated to be per-profile

View file

@ -13,6 +13,11 @@ export const providerProfilesSchema = z.object({
currentApiConfigName: z.string(),
apiConfigs: z.record(z.string(), providerSettingsWithIdSchema),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
migrations: z
.object({
rateLimitSecondsMigrated: z.boolean().optional(),
})
.optional(),
})
export type ProviderProfiles = z.infer<typeof providerProfilesSchema>
@ -27,8 +32,16 @@ export class ProviderSettingsManager {
private readonly defaultProviderProfiles: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: { default: { id: this.defaultConfigId } },
apiConfigs: {
default: {
id: this.defaultConfigId,
rateLimitSeconds: 0,
},
},
modeApiConfigs: this.defaultModeApiConfigs,
migrations: {
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
},
}
private readonly context: ExtensionContext
@ -53,7 +66,7 @@ export class ProviderSettingsManager {
}
/**
* Initialize config if it doesn't exist.
* Initialize config if it doesn't exist and run migrations.
*/
public async initialize() {
try {
@ -75,6 +88,18 @@ export class ProviderSettingsManager {
}
}
// Ensure migrations field exists
if (!providerProfiles.migrations) {
providerProfiles.migrations = { rateLimitSecondsMigrated: false } // Initialize with default values
isDirty = true
}
if (!providerProfiles.migrations.rateLimitSecondsMigrated) {
await this.migrateRateLimitSeconds(providerProfiles)
providerProfiles.migrations.rateLimitSecondsMigrated = true
isDirty = true
}
if (isDirty) {
await this.store(providerProfiles)
}
@ -84,6 +109,36 @@ export class ProviderSettingsManager {
}
}
private async migrateRateLimitSeconds(providerProfiles: ProviderProfiles) {
try {
let rateLimitSeconds: number | undefined
try {
rateLimitSeconds = await this.context.globalState.get<number>("rateLimitSeconds")
} catch (error) {
console.error("[MigrateRateLimitSeconds] Error getting global rate limit:", error)
}
if (rateLimitSeconds === undefined) {
// Failed to get the existing value, use the default
rateLimitSeconds = 0
}
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
if (apiConfig.rateLimitSeconds === undefined) {
console.log(
`[MigrateRateLimitSeconds] Applying rate limit ${rateLimitSeconds}s to profile: ${name}`,
)
apiConfig.rateLimitSeconds = rateLimitSeconds
}
}
console.log(`[MigrateRateLimitSeconds] migration complete`)
} catch (error) {
console.error(`[MigrateRateLimitSeconds] Failed to migrate rate limit settings:`, error)
}
}
/**
* List all available configs with metadata.
*/

View file

@ -12,8 +12,14 @@ const mockSecrets = {
delete: jest.fn(),
}
const mockGlobalState = {
get: jest.fn(),
update: jest.fn(),
}
const mockContext = {
secrets: mockSecrets,
globalState: mockGlobalState,
} as unknown as ExtensionContext
describe("ProviderSettingsManager", () => {
@ -45,6 +51,9 @@ describe("ProviderSettingsManager", () => {
id: "default",
},
},
migrations: {
rateLimitSecondsMigrated: true,
},
}),
)
@ -78,6 +87,43 @@ describe("ProviderSettingsManager", () => {
expect(storedConfig.apiConfigs.test.id).toBeTruthy()
})
it("should call migrateRateLimitSeconds if it has not done so already", async () => {
mockGlobalState.get.mockResolvedValue(42)
mockSecrets.get.mockResolvedValue(
JSON.stringify({
currentApiConfigName: "default",
apiConfigs: {
default: {
config: {},
id: "default",
rateLimitSeconds: undefined,
},
test: {
apiProvider: "anthropic",
rateLimitSeconds: undefined,
},
existing: {
apiProvider: "anthropic",
// this should not really be possible, unless someone has loaded a hand edited config,
// but we don't overwrite so we'll check that
rateLimitSeconds: 43,
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}),
)
await providerSettingsManager.initialize()
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[1][1])
expect(storedConfig.apiConfigs.default.rateLimitSeconds).toEqual(42)
expect(storedConfig.apiConfigs.test.rateLimitSeconds).toEqual(42)
expect(storedConfig.apiConfigs.existing.rateLimitSeconds).toEqual(43)
})
it("should throw error if secrets storage fails", async () => {
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
@ -105,6 +151,9 @@ describe("ProviderSettingsManager", () => {
architect: "default",
ask: "default",
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
@ -125,6 +174,9 @@ describe("ProviderSettingsManager", () => {
architect: "default",
ask: "default",
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
@ -201,6 +253,9 @@ describe("ProviderSettingsManager", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
@ -221,6 +276,9 @@ describe("ProviderSettingsManager", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
expect(mockSecrets.store).toHaveBeenCalledWith(
@ -257,6 +315,9 @@ describe("ProviderSettingsManager", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
@ -312,8 +373,12 @@ describe("ProviderSettingsManager", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockGlobalState.get.mockResolvedValue(42)
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const config = await providerSettingsManager.loadConfig("test")
@ -325,7 +390,7 @@ describe("ProviderSettingsManager", () => {
})
// Get the stored config to check the structure
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[1][1])
expect(storedConfig.currentApiConfigName).toBe("test")
expect(storedConfig.apiConfigs.test).toEqual({
apiProvider: "anthropic",
@ -409,6 +474,9 @@ describe("ProviderSettingsManager", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))

View file

@ -121,6 +121,7 @@ describe("importExport", () => {
},
},
}
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
// Mock listConfig
@ -294,6 +295,9 @@ describe("importExport", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
}
mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
@ -345,6 +349,9 @@ describe("importExport", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
})
// Mock global settings
@ -384,6 +391,9 @@ describe("importExport", () => {
id: "test-id",
},
},
migrations: {
rateLimitSecondsMigrated: false,
},
})
// Mock global settings

View file

@ -177,6 +177,7 @@ type ProviderSettings = {
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
rateLimitSeconds?: number | undefined
fakeAi?: unknown | undefined
}

View file

@ -178,6 +178,7 @@ type ProviderSettings = {
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
rateLimitSeconds?: number | undefined
fakeAi?: unknown | undefined
}

View file

@ -386,6 +386,7 @@ export const providerSettingsSchema = z.object({
modelMaxThinkingTokens: z.number().optional(),
// Generic
includeMaxTokens: z.boolean().optional(),
rateLimitSeconds: z.number().optional(),
// Fake AI
fakeAi: z.unknown().optional(),
})
@ -470,6 +471,7 @@ const providerSettingsRecord: ProviderSettingsRecord = {
modelMaxThinkingTokens: undefined,
// Generic
includeMaxTokens: undefined,
rateLimitSeconds: undefined,
// Fake AI
fakeAi: undefined,
}

View file

@ -11,13 +11,11 @@ import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
rateLimitSeconds: number
diffEnabled?: boolean
fuzzyMatchThreshold?: number
setCachedStateField: SetCachedStateField<"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold">
setCachedStateField: SetCachedStateField<"diffEnabled" | "fuzzyMatchThreshold">
}
export const AdvancedSettings = ({
rateLimitSeconds,
diffEnabled,
fuzzyMatchThreshold,
setCachedStateField,
@ -36,25 +34,6 @@ export const AdvancedSettings = ({
</SectionHeader>
<Section>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:advanced.rateLimit.label")}</span>
<div className="flex items-center gap-2">
<Slider
min={0}
max={60}
step={1}
value={[rateLimitSeconds]}
onValueChange={([value]) => setCachedStateField("rateLimitSeconds", value)}
/>
<span className="w-10">{rateLimitSeconds}s</span>
</div>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:advanced.rateLimit.description")}
</div>
</div>
<div>
<VSCodeCheckbox
checked={diffEnabled}

View file

@ -52,6 +52,7 @@ import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
import { ModelInfoView } from "./ModelInfoView"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
import { ApiErrorMessage } from "./ApiErrorMessage"
import { ThinkingBudget } from "./ThinkingBudget"
import { R1FormatSetting } from "./R1FormatSetting"
@ -1623,11 +1624,17 @@ const ApiOptions = ({
)}
{!fromWelcomeView && (
<TemperatureControl
value={apiConfiguration?.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
/>
<>
<TemperatureControl
value={apiConfiguration?.modelTemperature}
onChange={handleInputChange("modelTemperature", noTransform)}
maxValue={2}
/>
<RateLimitSecondsControl
value={apiConfiguration.rateLimitSeconds || 0}
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
/>
</>
)}
</div>
)

View file

@ -0,0 +1,38 @@
import React, { useCallback } from "react"
import { Slider } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
interface RateLimitSecondsControlProps {
value: number
onChange: (value: number) => void
}
export const RateLimitSecondsControl: React.FC<RateLimitSecondsControlProps> = ({ value, onChange }) => {
const { t } = useAppTranslation()
const handleValueChange = useCallback(
(newValue: number) => {
onChange(newValue)
},
[onChange],
)
return (
<div className="flex flex-col gap-1">
<label className="block font-medium mb-1">{t("settings:providers.rateLimitSeconds.label")}</label>
<div className="flex items-center gap-2">
<Slider
value={[value]}
min={0}
max={60}
step={1}
onValueChange={(newValue) => handleValueChange(newValue[0])}
/>
<span className="w-10">{value}s</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.rateLimitSeconds.description", { value })}
</div>
</div>
)
}

View file

@ -119,7 +119,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxOpenTabsContext,
maxWorkspaceFiles,
mcpEnabled,
rateLimitSeconds,
requestDelaySeconds,
remoteBrowserHost,
screenshotQuality,
@ -241,7 +240,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
@ -489,7 +487,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<div ref={advancedRef}>
<AdvancedSettings
rateLimitSeconds={rateLimitSeconds}
diffEnabled={diffEnabled}
fuzzyMatchThreshold={fuzzyMatchThreshold}
setCachedStateField={setCachedStateField}

View file

@ -71,6 +71,21 @@ jest.mock("../TemperatureControl", () => ({
),
}))
jest.mock("../RateLimitSecondsControl", () => ({
RateLimitSecondsControl: ({ value, onChange }: any) => (
<div data-testid="rate-limit-seconds-control">
<input
type="range"
value={value || 0}
onChange={(e) => onChange(parseFloat(e.target.value))}
min={0}
max={60}
step={1}
/>
</div>
),
}))
// Mock ThinkingBudget component
jest.mock("../ThinkingBudget", () => ({
ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
@ -101,14 +116,16 @@ const renderApiOptions = (props = {}) => {
}
describe("ApiOptions", () => {
it("shows temperature control by default", () => {
it("shows temperature and rate limit controls by default", () => {
renderApiOptions()
expect(screen.getByTestId("temperature-control")).toBeInTheDocument()
expect(screen.getByTestId("rate-limit-seconds-control")).toBeInTheDocument()
})
it("hides temperature control when fromWelcomeView is true", () => {
it("hides temperature and rate limit controls when fromWelcomeView is true", () => {
renderApiOptions({ fromWelcomeView: true })
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
expect(screen.queryByTestId("rate-limit-seconds-control")).not.toBeInTheDocument()
})
describe("thinking functionality", () => {

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Restablir als valors per defecte"
},
"rateLimitSeconds": {
"label": "Límit de freqüència",
"description": "Temps mínim entre sol·licituds d'API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Límit de freqüència",
"description": "Temps mínim entre sol·licituds d'API."
},
"diff": {
"label": "Habilitar edició mitjançant diffs",
"description": "Quan està habilitat, Roo podrà editar fitxers més ràpidament i rebutjarà automàticament escriptures completes de fitxers truncats. Funciona millor amb l'últim model Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Auf Standardwerte zurücksetzen"
},
"rateLimitSeconds": {
"label": "Ratenbegrenzung",
"description": "Minimale Zeit zwischen API-Anfragen."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Ratenbegrenzung",
"description": "Minimale Zeit zwischen API-Anfragen."
},
"diff": {
"label": "Bearbeitung durch Diffs aktivieren",
"description": "Wenn aktiviert, kann Roo Dateien schneller bearbeiten und lehnt automatisch gekürzte vollständige Dateischreibvorgänge ab. Funktioniert am besten mit dem neuesten Claude 3.7 Sonnet-Modell.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Reset to Defaults"
},
"rateLimitSeconds": {
"label": "Rate limit",
"description": "Minimum time between API requests."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Rate limit",
"description": "Minimum time between API requests."
},
"diff": {
"label": "Enable editing through diffs",
"description": "When enabled, Roo will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Restablecer valores predeterminados"
},
"rateLimitSeconds": {
"label": "Límite de tasa",
"description": "Tiempo mínimo entre solicitudes de API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Límite de tasa",
"description": "Tiempo mínimo entre solicitudes de API."
},
"diff": {
"label": "Habilitar edición a través de diffs",
"description": "Cuando está habilitado, Roo podrá editar archivos más rápidamente y rechazará automáticamente escrituras completas de archivos truncados. Funciona mejor con el último modelo Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Réinitialiser les valeurs par défaut"
},
"rateLimitSeconds": {
"label": "Limite de débit",
"description": "Temps minimum entre les requêtes API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Limite de débit",
"description": "Temps minimum entre les requêtes API."
},
"diff": {
"label": "Activer l'édition via des diffs",
"description": "Lorsque cette option est activée, Roo pourra éditer des fichiers plus rapidement et rejettera automatiquement les écritures de fichiers complets tronqués. Fonctionne mieux avec le dernier modèle Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "डिफ़ॉल्ट पर रीसेट करें"
},
"rateLimitSeconds": {
"label": "दर सीमा",
"description": "API अनुरोधों के बीच न्यूनतम समय।"
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "दर सीमा",
"description": "API अनुरोधों के बीच न्यूनतम समय।"
},
"diff": {
"label": "diffs के माध्यम से संपादन सक्षम करें",
"description": "जब सक्षम होता है, Roo फाइलों को तेजी से संपादित कर सकेगा और स्वचालित रूप से काटे गए पूर्ण-फाइल लेखन को अस्वीकार करेगा। नवीनतम Claude 3.7 Sonnet मॉडल के साथ सबसे अच्छा काम करता है।",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Ripristina valori predefiniti"
},
"rateLimitSeconds": {
"label": "Limite di frequenza",
"description": "Tempo minimo tra le richieste API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Limite di frequenza",
"description": "Tempo minimo tra le richieste API."
},
"diff": {
"label": "Abilita modifica tramite diff",
"description": "Quando abilitato, Roo sarà in grado di modificare i file più velocemente e rifiuterà automaticamente scritture di file completi troncati. Funziona meglio con l'ultimo modello Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "デフォルトにリセット"
},
"rateLimitSeconds": {
"label": "レート制限",
"description": "APIリクエスト間の最小時間。"
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "レート制限",
"description": "APIリクエスト間の最小時間。"
},
"diff": {
"label": "diff経由の編集を有効化",
"description": "有効にすると、Rooはファイルをより迅速に編集でき、切り詰められた全ファイル書き込みを自動的に拒否します。最新のClaude 3.7 Sonnetモデルで最良に機能します。",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "기본값으로 재설정"
},
"rateLimitSeconds": {
"label": "속도 제한",
"description": "API 요청 간 최소 시간."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "속도 제한",
"description": "API 요청 간 최소 시간."
},
"diff": {
"label": "diff를 통한 편집 활성화",
"description": "활성화되면 Roo는 파일을 더 빠르게 편집할 수 있으며 잘린 전체 파일 쓰기를 자동으로 거부합니다. 최신 Claude 3.7 Sonnet 모델에서 가장 잘 작동합니다.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Przywróć domyślne"
},
"rateLimitSeconds": {
"label": "Limit szybkości",
"description": "Minimalny czas między żądaniami API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Limit szybkości",
"description": "Minimalny czas między żądaniami API."
},
"diff": {
"label": "Włącz edycję przez różnice",
"description": "Gdy włączone, Roo będzie w stanie edytować pliki szybciej i automatycznie odrzuci obcięte pełne zapisy plików. Działa najlepiej z najnowszym modelem Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Restaurar Padrões"
},
"rateLimitSeconds": {
"label": "Limite de taxa",
"description": "Tempo mínimo entre requisições de API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Limite de taxa",
"description": "Tempo mínimo entre requisições de API."
},
"diff": {
"label": "Ativar edição através de diffs",
"description": "Quando ativado, o Roo poderá editar arquivos mais rapidamente e rejeitará automaticamente escritas completas de arquivos truncados. Funciona melhor com o modelo mais recente Claude 3.7 Sonnet.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Varsayılanlara Sıfırla"
},
"rateLimitSeconds": {
"label": "Hız sınırı",
"description": "API istekleri arasındaki minimum süre."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Hız sınırı",
"description": "API istekleri arasındaki minimum süre."
},
"diff": {
"label": "Diff'ler aracılığıyla düzenlemeyi etkinleştir",
"description": "Etkinleştirildiğinde, Roo dosyaları daha hızlı düzenleyebilecek ve kesik tam dosya yazımlarını otomatik olarak reddedecektir. En son Claude 3.7 Sonnet modeliyle en iyi şekilde çalışır.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "Đặt lại về mặc định"
},
"rateLimitSeconds": {
"label": "Giới hạn tốc độ",
"description": "Thời gian tối thiểu giữa các yêu cầu API."
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "Giới hạn tốc độ",
"description": "Thời gian tối thiểu giữa các yêu cầu API."
},
"diff": {
"label": "Bật chỉnh sửa qua diff",
"description": "Khi được bật, Roo sẽ có thể chỉnh sửa tệp nhanh hơn và sẽ tự động từ chối ghi toàn bộ tệp bị cắt ngắn. Hoạt động tốt nhất với mô hình Claude 3.7 Sonnet mới nhất.",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "重置为默认值"
},
"rateLimitSeconds": {
"label": "请求频率限制",
"description": "设置API请求的最小间隔时间"
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "请求频率限制",
"description": "设置API请求的最小间隔时间"
},
"diff": {
"label": "启用diff更新",
"description": "启用后Roo 将能够通过差异算法写入避免模型输出完整文件以降低Token消耗。与最新的 Claude 3.7 Sonnet 模型配合最佳。",

View file

@ -218,6 +218,10 @@
}
},
"resetDefaults": "重設為預設值"
},
"rateLimitSeconds": {
"label": "速率限制",
"description": "API 請求間的最短時間"
}
},
"browser": {
@ -298,10 +302,6 @@
}
},
"advanced": {
"rateLimit": {
"label": "速率限制",
"description": "API 請求間的最短時間"
},
"diff": {
"label": "透過差異比對編輯",
"description": "啟用後Roo 可更快速地編輯檔案,並自動拒絕不完整的整檔覆寫。搭配最新的 Claude 3.7 Sonnet 模型效果最佳。",