From 67b476285e4bab69337d56e063a35046964e29fd Mon Sep 17 00:00:00 2001 From: "Ton Hoang Nguyen (Bill)" <32552798+HahaBill@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:50:08 +0100 Subject: [PATCH] feat: adding `contextLimit` implementation from `maxContextWindow` PR + working with profile-specific thresholding --- src/api/providers/gemini.ts | 9 +- src/core/task/Task.ts | 5 +- .../src/components/settings/ApiOptions.tsx | 18 +- .../src/components/settings/SettingsView.tsx | 14 + .../components/settings/providers/Gemini.tsx | 396 +++++++++++++----- webview-ui/src/i18n/locales/en/settings.json | 12 + 6 files changed, 356 insertions(+), 98 deletions(-) diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 8790682f08..1fb10749b2 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -144,9 +144,16 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl override getModel() { const modelId = this.options.apiModelId let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId - const info: ModelInfo = geminiModels[id] + let info: ModelInfo = geminiModels[id] const params = getModelParams({ format: "gemini", modelId: id, model: info, settings: this.options }) + if (this.options.contextLimit) { + info = { + ...info, + contextWindow: this.options.contextLimit, + } + } + // The `:thinking` suffix indicates that the model is a "Hybrid" // reasoning model and that reasoning is required to be enabled. // The actual model ID honored by Gemini's API does not have this diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 46da7485ed..9487600fc0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1706,7 +1706,10 @@ export class Task extends EventEmitter { ? this.apiConfiguration.modelMaxTokens || DEFAULT_THINKING_MODEL_MAX_TOKENS : modelInfo.maxTokens - const contextWindow = modelInfo.contextWindow + const contextWindow = + this.apiConfiguration.apiProvider === "gemini" && this.apiConfiguration.contextLimit + ? this.apiConfiguration.contextLimit + : modelInfo.contextWindow const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 8f6050f4f2..f8e4764afc 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -74,6 +74,10 @@ export interface ApiOptionsProps { fromWelcomeView?: boolean errorMessage: string | undefined setErrorMessage: React.Dispatch> + currentProfileId?: string + profileThresholds?: Record + autoCondenseContextPercent?: number + setProfileThreshold?: (profileId: string, threshold: number) => void } const ApiOptions = ({ @@ -83,6 +87,10 @@ const ApiOptions = ({ fromWelcomeView, errorMessage, setErrorMessage, + currentProfileId, + profileThresholds, + autoCondenseContextPercent, + setProfileThreshold, }: ApiOptionsProps) => { const { t } = useAppTranslation() const { organizationAllowList } = useExtensionState() @@ -411,7 +419,15 @@ const ApiOptions = ({ )} {selectedProvider === "gemini" && ( - + )} {selectedProvider === "openai" && ( diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 839ce25b69..99b1407d97 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -233,6 +233,16 @@ const SettingsView = forwardRef(({ onDone, t }) }, []) + const setProfileThreshold = useCallback( + (profileId: string, threshold: number) => { + setCachedStateField("profileThresholds", { + ...profileThresholds, + [profileId]: threshold, + }) + }, + [profileThresholds, setCachedStateField], + ) + const setTelemetrySetting = useCallback((setting: TelemetrySetting) => { setCachedState((prevState) => { if (prevState.telemetrySetting === setting) { @@ -576,6 +586,10 @@ const SettingsView = forwardRef(({ onDone, t setApiConfigurationField={setApiConfigurationField} errorMessage={errorMessage} setErrorMessage={setErrorMessage} + currentProfileId={currentApiConfigName} + profileThresholds={profileThresholds || {}} + autoCondenseContextPercent={autoCondenseContextPercent || 75} + setProfileThreshold={setProfileThreshold} /> diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index 34cfd588a9..fd5051baf4 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/webview-ui/src/components/settings/providers/Gemini.tsx @@ -1,27 +1,101 @@ -import { useCallback, useState } from "react" +import { useCallback, useState, useMemo } from "react" import { Checkbox } from "vscrui" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { Slider } from "@src/components/ui" import type { ProviderSettings } from "@roo-code/types" +import { geminiModels, geminiDefaultModelId, type GeminiModelId } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { vscode } from "@src/utils/vscode" import { inputEventTransform } from "../transforms" type GeminiProps = { apiConfiguration: ProviderSettings setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + currentModelId?: string + currentProfileId?: string + profileThresholds?: Record + autoCondenseContextPercent?: number + setProfileThreshold?: (profileId: string, threshold: number) => void } -export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => { +export const Gemini = ({ + apiConfiguration, + setApiConfigurationField, + currentModelId, + currentProfileId, + profileThresholds = {}, + autoCondenseContextPercent = 75, + setProfileThreshold, +}: GeminiProps) => { const { t } = useAppTranslation() const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState( !!apiConfiguration?.googleGeminiBaseUrl, ) + const [isCustomContextLimit, setIsCustomContextLimit] = useState( + apiConfiguration?.contextLimit !== undefined && apiConfiguration?.contextLimit !== null, + ) + + const modelInfo = useMemo(() => { + const modelId = ( + currentModelId && currentModelId in geminiModels ? currentModelId : geminiDefaultModelId + ) as GeminiModelId + return geminiModels[modelId] + }, [currentModelId]) + + const getCurrentThreshold = useCallback(() => { + if (!currentProfileId) return autoCondenseContextPercent + + const profileThreshold = profileThresholds[currentProfileId] + if (profileThreshold === undefined || profileThreshold === -1) { + return autoCondenseContextPercent + } + return profileThreshold + }, [currentProfileId, profileThresholds, autoCondenseContextPercent]) + + const handleThresholdChange = useCallback( + (newThreshold: number) => { + if (!currentProfileId || !setProfileThreshold) return + + setProfileThreshold(currentProfileId, newThreshold) + + vscode.postMessage({ + type: "profileThresholds", + values: { + ...profileThresholds, + [currentProfileId]: newThreshold, + }, + }) + }, + [currentProfileId, profileThresholds, setProfileThreshold], + ) + + const getTriggerDetails = useCallback(() => { + const contextWindow = apiConfiguration?.contextLimit || modelInfo?.contextWindow || 1048576 + const threshold = getCurrentThreshold() + + const TOKEN_BUFFER_PERCENTAGE = 0.1 + const maxTokens = modelInfo?.maxTokens + const reservedTokens = maxTokens || contextWindow * 0.2 + const allowedTokens = Math.floor(contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens) + + const percentageBasedTrigger = Math.floor(contextWindow * (threshold / 100)) + + return { + percentageBasedTrigger, + allowedTokens, + actualTrigger: Math.min(percentageBasedTrigger, allowedTokens), + triggerReason: allowedTokens < percentageBasedTrigger ? "token-limit" : "percentage-threshold", + maxTokens, + reservedTokens, + } + }, [apiConfiguration?.contextLimit, modelInfo, getCurrentThreshold]) + const handleInputChange = useCallback( ( field: K, @@ -51,12 +125,12 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro {t("settings:providers.getGeminiApiKey")} )} +
{ setGoogleGeminiBaseUrlSelected(checked) - if (!checked) { setApiConfigurationField("googleGeminiBaseUrl", "") } @@ -73,104 +147,236 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro /> )}
-
- -
- setApiConfigurationField("topP", values[0])} - className="flex-grow" - /> - {(apiConfiguration.topP ?? 0).toFixed(2)} + +
+

Model Parameters

+ +
+ +
+ setApiConfigurationField("topP", values[0])} + className="flex-grow" + /> + {(apiConfiguration.topP ?? 0).toFixed(2)} +
+
+ {t("settings:providers.geminiParameters.topP.description")} +
-
- {t("settings:providers.geminiParameters.topP.description")} + +
+ +
+ setApiConfigurationField("topK", values[0])} + className="flex-grow" + /> + {apiConfiguration.topK ?? 0} +
+
+ {t("settings:providers.geminiParameters.topK.description")} +
+
+ +
+ +
+ setApiConfigurationField("maxOutputTokens", values[0])} + className="flex-grow" + /> + parseInt((e as any).target.value, 10))} + className="w-16" + /> +
+
+ {t("settings:providers.geminiParameters.maxOutputTokens.description")} +
-
- -
- setApiConfigurationField("topK", values[0])} - className="flex-grow" - /> - {apiConfiguration.topK ?? 0} -
-
- {t("settings:providers.geminiParameters.topK.description")} + +
+

{t("settings:providers.geminiContextManagement.title")}

+
+ { + setIsCustomContextLimit(checked) + if (!checked) { + setApiConfigurationField("contextLimit", null) + } else { + setApiConfigurationField( + "contextLimit", + apiConfiguration.contextLimit ?? modelInfo?.contextWindow ?? 1048576, + ) + } + }}> + + +
+ {t("settings:providers.geminiContextManagement.description")} +
+ +
+ {t("settings:providers.geminiContextManagement.modelDefault")}:{" "} + {(modelInfo?.contextWindow || 1048576).toLocaleString()} tokens +
+ + {isCustomContextLimit && ( +
+
+
+ setApiConfigurationField("contextLimit", value)} + /> + + parseInt((e as any).target.value, 10), + )} + className="w-24" + /> + tokens +
+
+
+ )}
+ + {currentProfileId && ( +
+ +
+ Context condensing threshold for this Gemini profile. When context reaches this percentage, + it will be automatically condensed. +
+ +
+ handleThresholdChange(value)} + className="flex-grow" + /> + { + const value = parseInt((e.target as HTMLInputElement).value, 10) + if (!isNaN(value) && value >= 5 && value <= 100) { + handleThresholdChange(value) + } + }} + className="w-16" + /> + % +
+ +
+ {(() => { + const details = getTriggerDetails() + return ( + <> +
+ Condensing will trigger at:{" "} + {details.actualTrigger.toLocaleString()} tokens + {details.triggerReason === "token-limit" && ( + + (due to token limit, not percentage) + + )} +
+
+ Available context window:{" "} + {( + apiConfiguration?.contextLimit || + modelInfo?.contextWindow || + 1048576 + ).toLocaleString()}{" "} + tokens +
+
+
+ • Percentage trigger: {details.percentageBasedTrigger.toLocaleString()}{" "} + tokens ({getCurrentThreshold()}%) +
+
+ • Token limit trigger: {details.allowedTokens.toLocaleString()} tokens +
+
+ •{" "} + + Actual trigger: {details.actualTrigger.toLocaleString()} tokens + +
+
+ + ) + })()} +
+
+ )}
-
- -
- setApiConfigurationField("maxOutputTokens", values[0])} - className="flex-grow" - /> - parseInt((e as any).target.value, 10))} - className="w-16" - /> + +
+

Advanced Features

+ + setApiConfigurationField("enableUrlContext", checked)}> + {t("settings:providers.geminiParameters.urlContext.title")} + +
+ {t("settings:providers.geminiParameters.urlContext.description")}
-
- {t("settings:providers.geminiParameters.maxOutputTokens.description")} -
-
- setApiConfigurationField("enableUrlContext", checked)}> - {t("settings:providers.geminiParameters.urlContext.title")} - -
- {t("settings:providers.geminiParameters.urlContext.description")} -
- setApiConfigurationField("enableGrounding", checked)}> - {t("settings:providers.geminiParameters.groundingSearch.title")} - -
- {t("settings:providers.geminiParameters.groundingSearch.description")} -
-
- -
- setApiConfigurationField("contextLimit", values[0])} - className="flex-grow" - /> - parseInt((e as any).target.value, 10))} - className="w-16" - /> -
-
- {t("settings:providers.geminiParameters.contextLimit.description")} + + setApiConfigurationField("enableGrounding", checked)}> + {t("settings:providers.geminiParameters.groundingSearch.title")} + +
+ {t("settings:providers.geminiParameters.groundingSearch.description")}
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 5a4b23b6b2..eb59833a84 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -236,6 +236,18 @@ "description": "Maximum number of previous messages to include in context. Lower values reduce token usage and costs but may limit conversation continuity." } }, + "geminiContextManagement": { + "title": "Gemini Context Management", + "useCustomContextWindow": "Use custom context window limit", + "description": "Override the model's default context window. When conversations approach this limit, Roo Code will automatically condense older messages.", + "modelDefault": "Model's default context window", + "condensingThreshold": { + "title": "Context Condensing Threshold", + "description": "Automatically condense context when it reaches this percentage of the context window.", + "triggerAt": "Condensing will trigger at", + "availableContext": "Available context window" + } + }, "googleCloudSetup": { "title": "To use Google Cloud Vertex AI, you need to:", "step1": "1. Create a Google Cloud account, enable the Vertex AI API & enable the desired Claude models.",