feat: adding contextLimit implementation from maxContextWindow PR + working with profile-specific thresholding

This commit is contained in:
Ton Hoang Nguyen (Bill) 2025-06-25 18:50:08 +01:00
parent a20774ead0
commit 67b476285e
6 changed files with 356 additions and 98 deletions

View file

@ -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

View file

@ -1706,7 +1706,10 @@ export class Task extends EventEmitter<ClineEvents> {
? 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,

View file

@ -74,6 +74,10 @@ export interface ApiOptionsProps {
fromWelcomeView?: boolean
errorMessage: string | undefined
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
currentProfileId?: string
profileThresholds?: Record<string, number>
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" && (
<Gemini apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
<Gemini
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
currentModelId={selectedModelId}
currentProfileId={currentProfileId}
profileThresholds={profileThresholds}
autoCondenseContextPercent={autoCondenseContextPercent}
setProfileThreshold={setProfileThreshold}
/>
)}
{selectedProvider === "openai" && (

View file

@ -233,6 +233,16 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
currentProfileId={currentApiConfigName}
profileThresholds={profileThresholds || {}}
autoCondenseContextPercent={autoCondenseContextPercent || 75}
setProfileThreshold={setProfileThreshold}
/>
</Section>
</div>

View file

@ -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<string, number>
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(
<K extends keyof ProviderSettings, E>(
field: K,
@ -51,12 +125,12 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro
{t("settings:providers.getGeminiApiKey")}
</VSCodeButtonLink>
)}
<div>
<Checkbox
checked={googleGeminiBaseUrlSelected}
onChange={(checked: boolean) => {
setGoogleGeminiBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("googleGeminiBaseUrl", "")
}
@ -73,104 +147,236 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro
/>
)}
</div>
<div className="mt-4">
<label className="block font-medium mb-1">{t("settings:providers.geminiParameters.topP.title")}</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={1}
step={0.01}
value={[apiConfiguration.topP ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("topP", values[0])}
className="flex-grow"
/>
<span className="w-10 text-right">{(apiConfiguration.topP ?? 0).toFixed(2)}</span>
<div className="mt-6 border-t border-vscode-widget-border pt-4">
<h3 className="font-semibold text-lg mb-4">Model Parameters</h3>
<div className="mt-4">
<label className="block font-medium mb-1">
{t("settings:providers.geminiParameters.topP.title")}
</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={1}
step={0.01}
value={[apiConfiguration.topP ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("topP", values[0])}
className="flex-grow"
/>
<span className="w-10 text-right">{(apiConfiguration.topP ?? 0).toFixed(2)}</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.topP.description")}
</div>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.topP.description")}
<div className="mt-4">
<label className="block font-medium mb-1">
{t("settings:providers.geminiParameters.topK.title")}
</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={100}
step={1}
value={[apiConfiguration.topK ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("topK", values[0])}
className="flex-grow"
/>
<span className="w-10 text-right">{apiConfiguration.topK ?? 0}</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.topK.description")}
</div>
</div>
<div className="mt-4">
<label className="block font-medium mb-1">
{t("settings:providers.geminiParameters.maxOutputTokens.title")}
</label>
<div className="flex items-center space-x-2">
<Slider
min={3000}
max={8192}
step={1}
value={[apiConfiguration.maxOutputTokens ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("maxOutputTokens", values[0])}
className="flex-grow"
/>
<VSCodeTextField
value={(apiConfiguration.maxOutputTokens ?? 0).toString()}
type="text"
inputMode="numeric"
onInput={handleInputChange("maxOutputTokens", (e) => parseInt((e as any).target.value, 10))}
className="w-16"
/>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.maxOutputTokens.description")}
</div>
</div>
</div>
<div className="mt-4">
<label className="block font-medium mb-1">{t("settings:providers.geminiParameters.topK.title")}</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={100}
step={1}
value={[apiConfiguration.topK ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("topK", values[0])}
className="flex-grow"
/>
<span className="w-10 text-right">{apiConfiguration.topK ?? 0}</span>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.topK.description")}
<div className="mt-6 border-t border-vscode-widget-border pt-4">
<h3 className="font-semibold text-lg mb-4">{t("settings:providers.geminiContextManagement.title")}</h3>
<div>
<Checkbox
checked={isCustomContextLimit}
onChange={(checked: boolean) => {
setIsCustomContextLimit(checked)
if (!checked) {
setApiConfigurationField("contextLimit", null)
} else {
setApiConfigurationField(
"contextLimit",
apiConfiguration.contextLimit ?? modelInfo?.contextWindow ?? 1048576,
)
}
}}>
<label className="block font-medium mb-1">
{t("settings:providers.geminiContextManagement.useCustomContextWindow")}
</label>
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mt-1 mb-3">
{t("settings:providers.geminiContextManagement.description")}
</div>
<div className="text-sm text-vscode-descriptionForeground mb-3">
<strong>{t("settings:providers.geminiContextManagement.modelDefault")}:</strong>{" "}
{(modelInfo?.contextWindow || 1048576).toLocaleString()} tokens
</div>
{isCustomContextLimit && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<div className="flex items-center gap-2">
<Slider
min={32000}
max={2097152}
step={1000}
value={[apiConfiguration.contextLimit ?? modelInfo?.contextWindow ?? 1048576]}
onValueChange={([value]) => setApiConfigurationField("contextLimit", value)}
/>
<VSCodeTextField
value={(
apiConfiguration.contextLimit ??
modelInfo?.contextWindow ??
1048576
).toString()}
type="text"
inputMode="numeric"
onInput={handleInputChange("contextLimit", (e) =>
parseInt((e as any).target.value, 10),
)}
className="w-24"
/>
<span className="text-sm">tokens</span>
</div>
</div>
</div>
)}
</div>
{currentProfileId && (
<div className="mt-6">
<label className="block font-medium mb-1">
{t("settings:providers.geminiContextManagement.condensingThreshold.title")}
</label>
<div className="text-sm text-vscode-descriptionForeground mb-3">
Context condensing threshold for this Gemini profile. When context reaches this percentage,
it will be automatically condensed.
</div>
<div className="flex items-center gap-2 mb-2">
<Slider
min={5}
max={100}
step={1}
value={[getCurrentThreshold()]}
onValueChange={([value]) => handleThresholdChange(value)}
className="flex-grow"
/>
<VSCodeTextField
value={getCurrentThreshold().toString()}
type="text"
inputMode="numeric"
onChange={(e) => {
const value = parseInt((e.target as HTMLInputElement).value, 10)
if (!isNaN(value) && value >= 5 && value <= 100) {
handleThresholdChange(value)
}
}}
className="w-16"
/>
<span className="text-sm">%</span>
</div>
<div className="text-sm text-vscode-descriptionForeground space-y-1">
{(() => {
const details = getTriggerDetails()
return (
<>
<div>
<strong>Condensing will trigger at:</strong>{" "}
{details.actualTrigger.toLocaleString()} tokens
{details.triggerReason === "token-limit" && (
<span className="text-yellow-600 ml-2">
(due to token limit, not percentage)
</span>
)}
</div>
<div>
<strong>Available context window:</strong>{" "}
{(
apiConfiguration?.contextLimit ||
modelInfo?.contextWindow ||
1048576
).toLocaleString()}{" "}
tokens
</div>
<div className="text-xs text-vscode-descriptionForeground">
<div>
Percentage trigger: {details.percentageBasedTrigger.toLocaleString()}{" "}
tokens ({getCurrentThreshold()}%)
</div>
<div>
Token limit trigger: {details.allowedTokens.toLocaleString()} tokens
</div>
<div>
{" "}
<strong>
Actual trigger: {details.actualTrigger.toLocaleString()} tokens
</strong>
</div>
</div>
</>
)
})()}
</div>
</div>
)}
</div>
<div className="mt-4">
<label className="block font-medium mb-1">
{t("settings:providers.geminiParameters.maxOutputTokens.title")}
</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={2048}
step={1}
value={[apiConfiguration.maxOutputTokens ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("maxOutputTokens", values[0])}
className="flex-grow"
/>
<VSCodeTextField
value={(apiConfiguration.maxOutputTokens ?? 0).toString()}
type="text"
inputMode="numeric"
onInput={handleInputChange("maxOutputTokens", (e) => parseInt((e as any).target.value, 10))}
className="w-16"
/>
<div className="mt-6 border-t border-vscode-widget-border pt-4">
<h3 className="font-semibold text-lg mb-4">Advanced Features</h3>
<Checkbox
checked={!!apiConfiguration.enableUrlContext}
onChange={(checked: boolean) => setApiConfigurationField("enableUrlContext", checked)}>
{t("settings:providers.geminiParameters.urlContext.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-3">
{t("settings:providers.geminiParameters.urlContext.description")}
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.maxOutputTokens.description")}
</div>
</div>
<Checkbox
checked={!!apiConfiguration.enableUrlContext}
onChange={(checked: boolean) => setApiConfigurationField("enableUrlContext", checked)}>
{t("settings:providers.geminiParameters.urlContext.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("settings:providers.geminiParameters.urlContext.description")}
</div>
<Checkbox
checked={!!apiConfiguration.enableGrounding}
onChange={(checked: boolean) => setApiConfigurationField("enableGrounding", checked)}>
{t("settings:providers.geminiParameters.groundingSearch.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("settings:providers.geminiParameters.groundingSearch.description")}
</div>
<div className="mt-4">
<label className="block font-medium mb-1">
{t("settings:providers.geminiParameters.contextLimit.title")}
</label>
<div className="flex items-center space-x-2">
<Slider
min={0}
max={2048}
step={1}
value={[apiConfiguration.contextLimit ?? 0]}
onValueChange={(values: number[]) => setApiConfigurationField("contextLimit", values[0])}
className="flex-grow"
/>
<VSCodeTextField
value={(apiConfiguration.contextLimit ?? 0).toString()}
type="text"
inputMode="numeric"
onInput={handleInputChange("contextLimit", (e) => parseInt((e as any).target.value, 10))}
className="w-16"
/>
</div>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.geminiParameters.contextLimit.description")}
<Checkbox
checked={!!apiConfiguration.enableGrounding}
onChange={(checked: boolean) => setApiConfigurationField("enableGrounding", checked)}>
{t("settings:providers.geminiParameters.groundingSearch.title")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mb-3">
{t("settings:providers.geminiParameters.groundingSearch.description")}
</div>
</div>
</>

View file

@ -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.",