mirror of
https://github.com/cline/cline.git
synced 2026-09-21 05:10:09 +08:00
fix ollama and lmtudio settings persistence
This commit is contained in:
committed by
Dominic Cooney
parent
7528fa0f01
commit
47ab282057
@@ -1,14 +1,16 @@
|
||||
import { type ModelInfo, openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { VSCodeDropdown, VSCodeLink, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useInterval } from "react-use"
|
||||
import UseCustomPromptCheckbox from "@/components/settings/UseCustomPromptCheckbox"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useProviderConfig } from "@/hooks/useProviderConfig"
|
||||
import { useProviderModelSelection } from "@/hooks/useProviderModelSelection"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import { DropdownContainer } from "../common/ModelSelector"
|
||||
import { getModeSpecificFields } from "../utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
|
||||
/**
|
||||
@@ -38,19 +40,68 @@ interface LMStudioApiModel {
|
||||
*/
|
||||
export const LMStudioProvider = ({ currentMode }: LMStudioProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
const { lmStudioModelId } = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
const { handleFieldChange } = useApiConfigurationHandlers()
|
||||
const { config, write, commitSelection } = useProviderConfig("lmstudio")
|
||||
|
||||
const [lmStudioModels, setLmStudioModels] = useState<LMStudioApiModel[]>([])
|
||||
const [pendingSelectedModelId, setPendingSelectedModelId] = useState<string | undefined>(undefined)
|
||||
|
||||
const toLmStudioModelInfo = useCallback((model: LMStudioApiModel | undefined, modelId: string): ModelInfo => {
|
||||
const contextWindow = model?.loaded_context_length ?? model?.max_context_length
|
||||
return {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
name: modelId,
|
||||
...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),
|
||||
...(model?.max_context_length !== undefined && model.max_context_length > 0
|
||||
? { maxTokens: model.max_context_length }
|
||||
: {}),
|
||||
}
|
||||
}, [])
|
||||
const lmStudioModelInfoById = useMemo(
|
||||
() => Object.fromEntries(lmStudioModels.map((model) => [model.id, toLmStudioModelInfo(model, model.id)])),
|
||||
[lmStudioModels, toLmStudioModelInfo],
|
||||
)
|
||||
const { selectedModel, commitModelSelection } = useProviderModelSelection("lmstudio", currentMode, {
|
||||
models: lmStudioModelInfoById,
|
||||
config,
|
||||
commitSelection,
|
||||
fallbackModelInfo: openAiModelInfoSafeDefaults,
|
||||
customModelInfo: (modelId) => toLmStudioModelInfo(undefined, modelId),
|
||||
})
|
||||
const displayedSelectedModelId = pendingSelectedModelId ?? selectedModel.modelId
|
||||
const currentLMStudioModel = useMemo(
|
||||
() => lmStudioModels.find((model) => model.id === lmStudioModelId),
|
||||
[lmStudioModels, lmStudioModelId],
|
||||
() => lmStudioModels.find((model) => model.id === displayedSelectedModelId),
|
||||
[displayedSelectedModelId, lmStudioModels],
|
||||
)
|
||||
const endpoint = useMemo(
|
||||
() => apiConfiguration?.lmStudioBaseUrl || "http://localhost:1234",
|
||||
[apiConfiguration?.lmStudioBaseUrl],
|
||||
() => config?.baseUrl ?? apiConfiguration?.lmStudioBaseUrl ?? "http://localhost:1234",
|
||||
[apiConfiguration?.lmStudioBaseUrl, config?.baseUrl],
|
||||
)
|
||||
|
||||
const handleBaseUrlChange = useCallback(
|
||||
(value: string) => {
|
||||
void write({ baseUrl: value }).catch((error) => console.error("Failed to update LM Studio base URL:", error))
|
||||
},
|
||||
[write],
|
||||
)
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
(modelId: string) => {
|
||||
const trimmedModelId = modelId.trim()
|
||||
if (!trimmedModelId) {
|
||||
return
|
||||
}
|
||||
setPendingSelectedModelId(trimmedModelId)
|
||||
const model = lmStudioModels.find((candidate) => candidate.id === trimmedModelId)
|
||||
void commitModelSelection({
|
||||
modelId: trimmedModelId,
|
||||
modelInfo: toLmStudioModelInfo(model, trimmedModelId),
|
||||
}).catch((error) => {
|
||||
console.error("Failed to update LM Studio model selection:", error)
|
||||
setPendingSelectedModelId(undefined)
|
||||
})
|
||||
},
|
||||
[commitModelSelection, lmStudioModels, toLmStudioModelInfo],
|
||||
)
|
||||
|
||||
// Poll LM Studio models
|
||||
@@ -71,11 +122,17 @@ export const LMStudioProvider = ({ currentMode }: LMStudioProviderProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
requestLmStudioModels()
|
||||
}, [])
|
||||
}, [requestLmStudioModels])
|
||||
|
||||
const lmStudioMaxTokens = currentLMStudioModel?.max_context_length?.toString()
|
||||
const currentLoadedContext = currentLMStudioModel?.loaded_context_length?.toString()
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingSelectedModelId && selectedModel.modelId === pendingSelectedModelId) {
|
||||
setPendingSelectedModelId(undefined)
|
||||
}
|
||||
}, [pendingSelectedModelId, selectedModel.modelId])
|
||||
|
||||
useEffect(() => {
|
||||
const curr = currentLMStudioModel?.loaded_context_length?.toString()
|
||||
const max = currentLMStudioModel?.max_context_length?.toString()
|
||||
@@ -95,9 +152,9 @@ export const LMStudioProvider = ({ currentMode }: LMStudioProviderProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseUrlField
|
||||
initialValue={apiConfiguration?.lmStudioBaseUrl}
|
||||
initialValue={config?.baseUrl ?? apiConfiguration?.lmStudioBaseUrl}
|
||||
label="Use custom base URL"
|
||||
onChange={(value) => handleFieldChange("lmStudioBaseUrl", value)}
|
||||
onChange={handleBaseUrlChange}
|
||||
placeholder="Default: http://localhost:1234"
|
||||
/>
|
||||
|
||||
@@ -108,16 +165,11 @@ export const LMStudioProvider = ({ currentMode }: LMStudioProviderProps) => {
|
||||
className="w-full mb-3"
|
||||
onChange={(e: any) => {
|
||||
const value = e?.target?.value
|
||||
handleModeFieldChange(
|
||||
{
|
||||
plan: "planModeLmStudioModelId",
|
||||
act: "actModeLmStudioModelId",
|
||||
},
|
||||
value,
|
||||
currentMode,
|
||||
)
|
||||
if (typeof value === "string") {
|
||||
handleModelChange(value)
|
||||
}
|
||||
}}
|
||||
value={lmStudioModelId}>
|
||||
value={displayedSelectedModelId}>
|
||||
{lmStudioModels.map((model) => (
|
||||
<VSCodeOption className="w-full" key={model.id} value={model.id}>
|
||||
{model.id}
|
||||
@@ -127,17 +179,8 @@ export const LMStudioProvider = ({ currentMode }: LMStudioProviderProps) => {
|
||||
</DropdownContainer>
|
||||
) : (
|
||||
<DebouncedTextField
|
||||
initialValue={lmStudioModelId || ""}
|
||||
onChange={(value) =>
|
||||
handleModeFieldChange(
|
||||
{
|
||||
plan: "planModeLmStudioModelId",
|
||||
act: "actModeLmStudioModelId",
|
||||
},
|
||||
value,
|
||||
currentMode,
|
||||
)
|
||||
}
|
||||
initialValue={displayedSelectedModelId || ""}
|
||||
onChange={handleModelChange}
|
||||
placeholder={"e.g. meta-llama-3.1-8b-instruct"}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { openAiModelInfoSafeDefaults } from "@shared/api"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useInterval } from "react-use"
|
||||
import UseCustomPromptCheckbox from "@/components/settings/UseCustomPromptCheckbox"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useProviderConfig } from "@/hooks/useProviderConfig"
|
||||
import { useProviderModelSelection } from "@/hooks/useProviderModelSelection"
|
||||
import { ModelsServiceClient } from "@/services/grpc-client"
|
||||
import { ApiKeyField } from "../common/ApiKeyField"
|
||||
import { BaseUrlField } from "../common/BaseUrlField"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import OllamaModelPicker from "../OllamaModelPicker"
|
||||
import { getModeSpecificFields } from "../utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { useProviderApiKeyField } from "../utils/useProviderApiKeyField"
|
||||
|
||||
/**
|
||||
* Props for the OllamaProvider component
|
||||
@@ -27,18 +30,49 @@ interface OllamaProviderProps {
|
||||
*/
|
||||
export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: OllamaProviderProps) => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
|
||||
const { ollamaModelId } = getModeSpecificFields(apiConfiguration, currentMode)
|
||||
const { handleFieldChange } = useApiConfigurationHandlers()
|
||||
const { config, write, commitSelection } = useProviderConfig("ollama")
|
||||
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
|
||||
const ollamaBaseUrl = config?.baseUrl ?? apiConfiguration?.ollamaBaseUrl
|
||||
const ollamaModelInfo = useMemo(() => {
|
||||
const contextWindow = Number.parseInt(apiConfiguration?.ollamaApiOptionsCtxNum || "", 10)
|
||||
return {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow } : {}),
|
||||
}
|
||||
}, [apiConfiguration?.ollamaApiOptionsCtxNum])
|
||||
const ollamaModelInfoById = useMemo(
|
||||
() => Object.fromEntries(ollamaModels.map((modelId) => [modelId, { ...ollamaModelInfo, name: modelId }])),
|
||||
[ollamaModelInfo, ollamaModels],
|
||||
)
|
||||
const { selectedModel, commitModelSelection } = useProviderModelSelection("ollama", currentMode, {
|
||||
models: ollamaModelInfoById,
|
||||
config,
|
||||
commitSelection,
|
||||
fallbackModelInfo: ollamaModelInfo,
|
||||
customModelInfo: (modelId) => ({ ...ollamaModelInfo, name: modelId }),
|
||||
})
|
||||
const { savedApiKeyMask, handleApiKeyChange } = useProviderApiKeyField({
|
||||
apiKeyLength: config?.apiKeyLength,
|
||||
providerName: "Ollama",
|
||||
write,
|
||||
})
|
||||
|
||||
const handleBaseUrlChange = useCallback(
|
||||
(value: string) => {
|
||||
void write({ baseUrl: value }).catch((error) => console.error("Failed to update Ollama base URL:", error))
|
||||
},
|
||||
[write],
|
||||
)
|
||||
|
||||
// Poll ollama models
|
||||
const requestOllamaModels = useCallback(async () => {
|
||||
try {
|
||||
const response = await ModelsServiceClient.getOllamaModels(
|
||||
StringRequest.create({
|
||||
value: apiConfiguration?.ollamaBaseUrl || "",
|
||||
value: ollamaBaseUrl || "",
|
||||
}),
|
||||
)
|
||||
if (response && response.values) {
|
||||
@@ -48,7 +82,7 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
console.error("Failed to fetch Ollama models:", error)
|
||||
setOllamaModels([])
|
||||
}
|
||||
}, [apiConfiguration?.ollamaBaseUrl])
|
||||
}, [ollamaBaseUrl])
|
||||
|
||||
useEffect(() => {
|
||||
requestOllamaModels()
|
||||
@@ -59,17 +93,17 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseUrlField
|
||||
initialValue={apiConfiguration?.ollamaBaseUrl}
|
||||
initialValue={ollamaBaseUrl}
|
||||
label="Use custom base URL"
|
||||
onChange={(value) => handleFieldChange("ollamaBaseUrl", value)}
|
||||
onChange={handleBaseUrlChange}
|
||||
placeholder="Default: http://localhost:11434"
|
||||
/>
|
||||
|
||||
{apiConfiguration?.ollamaBaseUrl && (
|
||||
{ollamaBaseUrl && (
|
||||
<ApiKeyField
|
||||
helpText="Optional API key for authenticated Ollama instances or cloud services. Leave empty for local installations."
|
||||
initialValue={apiConfiguration?.ollamaApiKey || ""}
|
||||
onChange={(value) => handleFieldChange("ollamaApiKey", value)}
|
||||
initialValue={savedApiKeyMask}
|
||||
onChange={handleApiKeyChange}
|
||||
placeholder="Enter API Key (optional)..."
|
||||
providerName="Ollama"
|
||||
/>
|
||||
@@ -82,10 +116,17 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
<OllamaModelPicker
|
||||
ollamaModels={ollamaModels}
|
||||
onModelChange={(modelId) => {
|
||||
handleModeFieldChange({ plan: "planModeOllamaModelId", act: "actModeOllamaModelId" }, modelId, currentMode)
|
||||
const trimmedModelId = modelId.trim()
|
||||
if (!trimmedModelId) {
|
||||
return
|
||||
}
|
||||
void commitModelSelection({
|
||||
modelId: trimmedModelId,
|
||||
modelInfo: { ...ollamaModelInfo, name: trimmedModelId },
|
||||
}).catch((error) => console.error("Failed to update Ollama model selection:", error))
|
||||
}}
|
||||
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
|
||||
selectedModelId={ollamaModelId || ""}
|
||||
selectedModelId={selectedModel.modelId || ""}
|
||||
/>
|
||||
|
||||
{/* Show status message based on model availability */}
|
||||
@@ -98,7 +139,21 @@ export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: Ollam
|
||||
|
||||
<DebouncedTextField
|
||||
initialValue={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
|
||||
onChange={(v) => handleFieldChange("ollamaApiOptionsCtxNum", v || undefined)}
|
||||
onChange={(v) => {
|
||||
handleFieldChange("ollamaApiOptionsCtxNum", v || undefined)
|
||||
|
||||
const contextWindow = Number.parseInt(v, 10)
|
||||
if (selectedModel.modelId) {
|
||||
void commitModelSelection({
|
||||
modelId: selectedModel.modelId,
|
||||
modelInfo: {
|
||||
...openAiModelInfoSafeDefaults,
|
||||
name: selectedModel.modelId,
|
||||
...(Number.isFinite(contextWindow) && contextWindow > 0 ? { contextWindow } : {}),
|
||||
},
|
||||
}).catch((error) => console.error("Failed to update Ollama context window:", error))
|
||||
}
|
||||
}}
|
||||
placeholder={"e.g. 32768"}
|
||||
style={{ width: "100%" }}>
|
||||
<span className="font-semibold">Model Context Window</span>
|
||||
|
||||
Reference in New Issue
Block a user