mirror of
https://github.com/cline/cline.git
synced 2026-10-25 10:26:36 +08:00
Address review feedback on Local Models
- Only delete LM Studio models on a local server with a single variant - Count every loaded LM Studio copy in memory totals - Cap the saved context window at the model's limit on Use with Cline - Clear the runtime model list cache after downloads and deletes - Hide the local model chip in SSH sessions - Ignore stale status refreshes and stale ClinePass lookups - Add fetchCurrentUserPlan to RpcClineAccountService - Label the Ollama response timeout as the default
This commit is contained in:
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
||||
import { statfs } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { clearPublicModelsCatalogCache } from "@cline/core";
|
||||
import {
|
||||
isLocalRuntime,
|
||||
LOCAL_MODEL_PULL_PROGRESS_EVENT,
|
||||
@@ -213,6 +214,8 @@ function startPull(
|
||||
};
|
||||
} finally {
|
||||
activePulls.delete(key);
|
||||
// The composer's model list comes from a cached copy of the runtime's.
|
||||
clearPublicModelsCatalogCache();
|
||||
broadcastEvent(ctx, LOCAL_MODEL_PULL_PROGRESS_EVENT, final);
|
||||
}
|
||||
})();
|
||||
@@ -264,6 +267,7 @@ export async function handleLocalModelsCommand(
|
||||
return {};
|
||||
case "delete_local_model":
|
||||
await backend.delete(readModel(args));
|
||||
clearPublicModelsCatalogCache();
|
||||
return {};
|
||||
case "show_local_model":
|
||||
return await backend.show(readModel(args));
|
||||
|
||||
@@ -37,6 +37,7 @@ interface LmStudioModel {
|
||||
reasoning?: unknown;
|
||||
};
|
||||
loaded_instances?: { id: string; config?: Record<string, unknown> }[];
|
||||
variants?: string[];
|
||||
}
|
||||
|
||||
/** A job from `POST /api/v1/models/download` and its status endpoint. */
|
||||
@@ -142,6 +143,13 @@ export function isNestedDir(parent: string, dir: string, depth: number) {
|
||||
|
||||
const hubModelsDir = (home: string) => path.join(home, "hub", "models");
|
||||
|
||||
/**
|
||||
* Deleting removes the model's whole source repos, which also hold any other
|
||||
* downloaded variants, and only makes sense when those files are on this computer.
|
||||
*/
|
||||
const canDelete = (model: LmStudioModel) =>
|
||||
isLocalUrl(readServerUrl()) && (model.variants?.length ?? 0) <= 1;
|
||||
|
||||
/**
|
||||
* Models downloaded from LM Studio Hub come with a manifest naming the
|
||||
* Hugging Face repos their files were downloaded from. LM Studio has no API
|
||||
@@ -251,14 +259,15 @@ export const lmstudioBackend: LocalRuntimeBackend = {
|
||||
quantization: model.quantization?.name ?? "",
|
||||
capabilities: readCapabilities(model),
|
||||
contextLength: model.max_context_length ?? 0,
|
||||
deletable: !!(await readHubSources(home, model.key)),
|
||||
deletable:
|
||||
canDelete(model) && !!(await readHubSources(home, model.key)),
|
||||
})),
|
||||
),
|
||||
running: models
|
||||
.filter((model) => model.loaded_instances?.length)
|
||||
.map((model) => ({
|
||||
name: model.key,
|
||||
sizeBytes: model.size_bytes,
|
||||
sizeBytes: model.size_bytes * (model.loaded_instances?.length ?? 1),
|
||||
contextLength:
|
||||
Number(model.loaded_instances?.[0]?.config?.context_length) || 0,
|
||||
})),
|
||||
@@ -324,6 +333,11 @@ export const lmstudioBackend: LocalRuntimeBackend = {
|
||||
},
|
||||
|
||||
async delete(model) {
|
||||
if (!canDelete(await findModel(model))) {
|
||||
throw new Error(
|
||||
"Cline can't delete this model. Remove it from LM Studio.",
|
||||
);
|
||||
}
|
||||
const home = await readHome();
|
||||
const sources = await readHubSources(home, model);
|
||||
if (!sources) {
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
import type { ProviderModel } from "@/lib/provider-schema";
|
||||
import { startStreamingTranscription } from "@/lib/streaming-transcription";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LOCAL_WORKSPACE_ENVIRONMENT_ID } from "@/lib/workspace-paths";
|
||||
import { LocalModelIndicator } from "./local-model-indicator";
|
||||
import { TokenUsageRing } from "./token-usage-ring";
|
||||
import { WorkspaceSelector as WorkspaceSelectorImpl } from "./workspace-selector";
|
||||
@@ -1707,7 +1708,10 @@ function ChatInputBarImpl({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{executionTarget !== "cloud" && isLocalRuntime(localRuntime) ? (
|
||||
{/* The chip controls this computer's runtime, which SSH sessions don't use. */}
|
||||
{executionTarget !== "cloud" &&
|
||||
environmentId === LOCAL_WORKSPACE_ENVIRONMENT_ID &&
|
||||
isLocalRuntime(localRuntime) ? (
|
||||
<LocalModelIndicator
|
||||
isBusy={isBusy}
|
||||
model={model}
|
||||
|
||||
@@ -202,6 +202,7 @@ export function AccountView() {
|
||||
ClineAccountOrganization[]
|
||||
>([]);
|
||||
const [clinePassModels, setClinePassModels] = useState<string[] | null>(null);
|
||||
const clinePassGenerationRef = useRef(0);
|
||||
const [overviewLoading, setOverviewLoading] = useState(true);
|
||||
const [overviewError, setOverviewError] = useState<string | null>(null);
|
||||
// Signed out is an expected state carried by a typed sidecar result (or a
|
||||
@@ -236,6 +237,7 @@ export function AccountView() {
|
||||
setBalance(null);
|
||||
setOrganizationBalance(null);
|
||||
setOrganizations([]);
|
||||
clinePassGenerationRef.current += 1;
|
||||
setClinePassModels(null);
|
||||
setUsageTransactions([]);
|
||||
setUsageLoaded(false);
|
||||
@@ -260,7 +262,15 @@ export function AccountView() {
|
||||
return;
|
||||
}
|
||||
// The subscription card is optional, so it neither delays nor breaks the page.
|
||||
void fetchClinePassModels().then(setClinePassModels, () => {});
|
||||
const generation = ++clinePassGenerationRef.current;
|
||||
void fetchClinePassModels().then(
|
||||
(models) => {
|
||||
if (clinePassGenerationRef.current === generation) {
|
||||
setClinePassModels(models);
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
const [balanceData, orgsData] = await Promise.all([
|
||||
fetchAccountBalance(),
|
||||
fetchAccountOrganizations(),
|
||||
|
||||
+1
-1
@@ -290,7 +290,7 @@ export function LocalModelDetailsDialog({
|
||||
<ReadOnlyField
|
||||
hint="Includes loading the model, so large models on slow hardware can hit it."
|
||||
label="Response timeout"
|
||||
value="5 minutes to start responding"
|
||||
value="5 minutes to start responding (default)"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -298,6 +298,7 @@ function RuntimePicker({
|
||||
export function LocalModelsContent() {
|
||||
const [runtime, setRuntime] = useState(readPreferredRuntime);
|
||||
const runtimeRef = useRef(runtime);
|
||||
const refreshGenerationRef = useRef(0);
|
||||
const [status, setStatus] = useState<LocalModelsStatus>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [pulls, setPulls] = useState<Record<string, LocalModelPullProgress>>(
|
||||
@@ -328,6 +329,7 @@ export function LocalModelsContent() {
|
||||
|
||||
const refresh = useCallback(async ({ quiet = false } = {}) => {
|
||||
if (!quiet) setRefreshing(true);
|
||||
const generation = ++refreshGenerationRef.current;
|
||||
try {
|
||||
const requested = runtimeRef.current;
|
||||
// Without a saved choice, the backend picks whichever runtime is set up.
|
||||
@@ -335,7 +337,10 @@ export function LocalModelsContent() {
|
||||
"get_local_models_status",
|
||||
requested ? { runtime: requested } : undefined,
|
||||
);
|
||||
if (requested !== runtimeRef.current) {
|
||||
if (
|
||||
requested !== runtimeRef.current ||
|
||||
generation !== refreshGenerationRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!requested) {
|
||||
@@ -491,8 +496,13 @@ export function LocalModelsContent() {
|
||||
}),
|
||||
);
|
||||
|
||||
const switchToModel = (model: string, contextWindow: number) =>
|
||||
const switchToModel = (model: string, preferredContextWindow: number) =>
|
||||
runAction(model, "using", async () => {
|
||||
// Runtimes won't load past the model's own limit, so chat mustn't assume more.
|
||||
const limit = installed.get(model)?.contextLength;
|
||||
const contextWindow = limit
|
||||
? Math.min(preferredContextWindow, limit)
|
||||
: preferredContextWindow;
|
||||
const provider = shownRuntime;
|
||||
await desktopClient.invoke("save_provider_settings", {
|
||||
provider,
|
||||
@@ -748,7 +758,7 @@ export function LocalModelsContent() {
|
||||
title={
|
||||
installedModel.deletable
|
||||
? undefined
|
||||
: `Only models downloaded from LM Studio Hub can be deleted here. Remove this one from ${runtimeName}.`
|
||||
: `Cline can only delete LM Studio Hub models with one variant, on this computer. Remove this one in ${runtimeName}.`
|
||||
}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -192,7 +192,7 @@ To change an LM Studio model's sampling or load defaults, open **My Models** in
|
||||
|
||||
Click **Delete** on a model, then confirm, to remove it from your disk. The confirmation shows how much space you'll get back. If the model is loaded, Cline unloads it first. You can download it again at any time.
|
||||
|
||||
With LM Studio, Cline can delete models downloaded from the LM Studio Hub, which includes every model you download from this page. Remove other models, such as ones you added from Hugging Face in LM Studio, from LM Studio's **My Models**.
|
||||
With LM Studio, Cline can delete models downloaded from the LM Studio Hub, which includes every model you download from this page, when LM Studio runs on this computer. Remove other models, such as ones you added from Hugging Face or models with more than one downloaded variant, from LM Studio's **My Models**.
|
||||
|
||||
## Which models are recommended
|
||||
|
||||
|
||||
@@ -182,6 +182,13 @@ export class RpcClineAccountService implements ClineAccountOperations {
|
||||
});
|
||||
}
|
||||
|
||||
public async fetchCurrentUserPlan(): Promise<UserCurrentPlan | undefined> {
|
||||
return this.request<UserCurrentPlan | undefined>({
|
||||
action: "clineAccount",
|
||||
operation: "fetchCurrentUserPlan",
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(request: ClineAccountActionRequest): Promise<T> {
|
||||
const response = await this.executor.runProviderAction(request);
|
||||
return response.result as T;
|
||||
|
||||
@@ -1066,6 +1066,7 @@ export {
|
||||
export {
|
||||
clearLiveModelsCatalogCache,
|
||||
clearPrivateModelsCatalogCache,
|
||||
clearPublicModelsCatalogCache,
|
||||
DEFAULT_MODELS_CATALOG_URL,
|
||||
getLiveModelsCatalog,
|
||||
getProviderConfig,
|
||||
|
||||
Reference in New Issue
Block a user