Persist provider/model between plan/act mode (#1525)

Fix truncation algorithm

Fix

Fix
This commit is contained in:
Saoud Rizwan 2025-01-28 15:26:42 -08:00 committed by GitHub
parent fc5d0bdb5a
commit ac53dbb122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 128 additions and 26 deletions

View file

@ -1272,10 +1272,16 @@ export class Cline {
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
this.conversationHistoryDeletedRange = getNextTruncationRange(
this.apiConversationHistory,
this.conversationHistoryDeletedRange,
keep,
)
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
// await this.overwriteApiConversationHistory(truncatedMessages)

View file

@ -55,13 +55,21 @@ truncated = getTruncatedMessages(messages, deletedRange);
export function getNextTruncationRange(
messages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined = undefined,
keep: "half" | "quarter" = "half",
): [number, number] {
// Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
const rangeStartIndex = 1
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1
// Remove half of user-assistant pairs
const messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
let messagesToRemove: number
if (keep === "half") {
// Remove half of user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of user-assistant pairs
messagesToRemove = Math.floor((messages.length - startOfRest) / 8) * 3 * 2
}
let rangeEndIndex = startOfRest + messagesToRemove - 1
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.

View file

@ -74,6 +74,9 @@ type GlobalStateKey =
| "vsCodeLmModelSelector"
| "localeLanguage"
| "userInfo"
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@ -501,6 +504,71 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "chatSettings":
if (message.chatSettings) {
const didSwitchToActMode = message.chatSettings.mode === "act"
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
}
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
}
}
await this.updateGlobalState("chatSettings", message.chatSettings)
await this.postStateToWebview()
if (this.cline) {
@ -1198,10 +1266,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserSettings,
chatSettings,
userInfo,
authToken,
localeLanguage,
} = await this.getState()
const authToken = await this.getSecret("authToken")
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@ -1310,6 +1378,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
vsCodeLmModelSelector,
localeLanguage,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@ -1346,6 +1418,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("localeLanguage") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
])
let apiProvider: ApiProvider
@ -1400,6 +1476,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
localeLanguage,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
}
}

View file

@ -586,20 +586,40 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
[updateCursorPosition],
)
// Separate the API config submission logic
const submitApiConfig = useCallback(() => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
} else {
vscode.postMessage({ type: "getLatestState" })
}
}, [apiConfiguration, openRouterModels])
const onModeToggle = useCallback(() => {
if (textAreaDisabled) return
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
vscode.postMessage({
type: "chatSettings",
chatSettings: {
mode: newMode,
},
})
// Focus the textarea after mode toggle with slight delay
let changeModeDelay = 0
if (showModelSelector) {
// user has model selector open, so we should save it before switching modes
submitApiConfig()
changeModeDelay = 250 // necessary to let the api config update (we send message and wait for it to be saved) FIXME: this is a hack and we ideally should check for api config changes, then wait for it to be saved, before switching modes
}
setTimeout(() => {
textAreaRef.current?.focus()
}, 100)
}, [chatSettings.mode, textAreaDisabled])
const newMode = chatSettings.mode === "plan" ? "act" : "plan"
vscode.postMessage({
type: "chatSettings",
chatSettings: {
mode: newMode,
},
})
// Focus the textarea after mode toggle with slight delay
setTimeout(() => {
textAreaRef.current?.focus()
}, 100)
}, changeModeDelay)
}, [chatSettings.mode, textAreaDisabled, showModelSelector, submitApiConfig])
const handleContextButtonClick = useCallback(() => {
if (textAreaDisabled) return
@ -644,18 +664,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
updateHighlights()
}, [inputValue, textAreaDisabled, handleInputChange, updateHighlights])
// Separate the API config submission logic
const submitApiConfig = useCallback(() => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
} else {
vscode.postMessage({ type: "getLatestState" })
}
}, [apiConfiguration, openRouterModels])
// Use an effect to detect menu close
useEffect(() => {
if (prevShowModelSelector.current && !showModelSelector) {