Fix download progress bugs: false completion, stale UI, dedup
Three fixes on top of the download progress feature: 1. Backend: Replace broken "no .incomplete = done" completion check with a 95% byte threshold. HF downloads files sequentially, so between files there are briefly no .incomplete files even though the download is far from done (e.g. Kimi-K2.5 reported "done" after downloading 22KB of config files out of 595GB). 2. Frontend: Track hasShownProgress flag. Only show "Download complete. Loading into memory..." if we actually displayed download progress before. For already-cached models where the first poll returns progress=1.0, this avoids the misleading "Download complete" message. 3. Frontend: Deduplicate recommended vs downloaded -- filter out models already in the "Downloaded" section. Cache the fetched lists at module level so re-mounting the popover does not flash an empty "Downloaded" section.
This commit is contained in:
parent
e03a809994
commit
1471c63b96
3 changed files with 36 additions and 13 deletions
|
|
@ -738,9 +738,12 @@ async def get_download_progress(
|
|||
"progress": 0,
|
||||
}
|
||||
|
||||
# No .incomplete files means download is done regardless of byte totals
|
||||
# (blob deduplication can make completed_bytes differ from expected_bytes)
|
||||
if in_progress_bytes == 0 and completed_bytes > 0:
|
||||
# Use 95% threshold for completion (blob deduplication can make
|
||||
# completed_bytes differ slightly from expected_bytes).
|
||||
# Do NOT use "no .incomplete files" as a completion signal --
|
||||
# HF downloads files sequentially, so between files there are
|
||||
# no .incomplete files even though the download is far from done.
|
||||
if completed_bytes >= expected_bytes * 0.95:
|
||||
progress = 1.0
|
||||
else:
|
||||
progress = min(downloaded_bytes / expected_bytes, 0.99)
|
||||
|
|
|
|||
|
|
@ -340,6 +340,10 @@ function isGgufRepo(id: string): boolean {
|
|||
return id.toUpperCase().includes("-GGUF");
|
||||
}
|
||||
|
||||
// Module-level caches so re-mounting the popover shows results instantly
|
||||
let _cachedGgufCache: CachedGgufRepo[] = [];
|
||||
let _cachedModelsCache: CachedModelRepo[] = [];
|
||||
|
||||
// ── Hub Model Picker ──────────────────────────────────────────
|
||||
|
||||
export function HubModelPicker({
|
||||
|
|
@ -361,17 +365,27 @@ export function HubModelPicker({
|
|||
// Track which GGUF repo is expanded for variant selection
|
||||
const [expandedGguf, setExpandedGguf] = useState<string | null>(null);
|
||||
|
||||
// Cached (already downloaded) repos
|
||||
const [cachedGguf, setCachedGguf] = useState<CachedGgufRepo[]>([]);
|
||||
const [cachedModels, setCachedModels] = useState<CachedModelRepo[]>([]);
|
||||
// Cached (already downloaded) repos -- use module-level cache so
|
||||
// re-mounting the popover does not flash an empty "Downloaded" section.
|
||||
const [cachedGguf, setCachedGguf] = useState<CachedGgufRepo[]>(_cachedGgufCache);
|
||||
const [cachedModels, setCachedModels] = useState<CachedModelRepo[]>(_cachedModelsCache);
|
||||
useEffect(() => {
|
||||
listCachedGguf().then(setCachedGguf).catch(() => {});
|
||||
listCachedModels().then(setCachedModels).catch(() => {});
|
||||
listCachedGguf().then((v) => { _cachedGgufCache = v; setCachedGguf(v); }).catch(() => {});
|
||||
listCachedModels().then((v) => { _cachedModelsCache = v; setCachedModels(v); }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Deduplicate: don't show downloaded models in the recommended list
|
||||
const downloadedSet = useMemo(() => {
|
||||
const s = new Set<string>();
|
||||
for (const c of cachedGguf) s.add(c.repo_id);
|
||||
for (const c of cachedModels) s.add(c.repo_id);
|
||||
return s;
|
||||
}, [cachedGguf, cachedModels]);
|
||||
|
||||
const recommendedIds = useMemo(
|
||||
() => dedupe([...models.map((model) => model.id), value ?? ""]),
|
||||
[models, value],
|
||||
() => dedupe([...models.map((model) => model.id), value ?? ""])
|
||||
.filter((id) => !downloadedSet.has(id)),
|
||||
[models, value, downloadedSet],
|
||||
);
|
||||
|
||||
const { paramCountById: recommendedParamCountById } =
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@ export function useChatModelRuntime() {
|
|||
},
|
||||
};
|
||||
|
||||
let hasShownProgress = false;
|
||||
|
||||
const pollProgress = async () => {
|
||||
// Stop if cancelled or if loading already finished
|
||||
if (abortCtrl.signal.aborted || !loadingModelRef.current) {
|
||||
|
|
@ -361,7 +363,8 @@ export function useChatModelRuntime() {
|
|||
// Re-check after await -- load may have finished while polling
|
||||
if (!loadingModelRef.current) return;
|
||||
|
||||
if (prog.downloaded_bytes > 0 && prog.progress > 0 && prog.progress < 1) {
|
||||
if (prog.progress > 0 && prog.progress < 1) {
|
||||
hasShownProgress = true;
|
||||
const dlGb = prog.downloaded_bytes / (1024 ** 3);
|
||||
const totalGb = prog.expected_bytes / (1024 ** 3);
|
||||
const pct = Math.round(prog.progress * 100);
|
||||
|
|
@ -376,7 +379,9 @@ export function useChatModelRuntime() {
|
|||
action: cancelAction,
|
||||
},
|
||||
);
|
||||
} else if (prog.downloaded_bytes > 0 && prog.expected_bytes === 0) {
|
||||
} else if (prog.downloaded_bytes > 0 && prog.expected_bytes === 0 && prog.progress === 0) {
|
||||
// Have bytes but no total size -- show bytes only
|
||||
hasShownProgress = true;
|
||||
const dlGb = prog.downloaded_bytes / (1024 ** 3);
|
||||
toast.loading(
|
||||
"Downloading model...",
|
||||
|
|
@ -387,7 +392,8 @@ export function useChatModelRuntime() {
|
|||
action: cancelAction,
|
||||
},
|
||||
);
|
||||
} else if (prog.progress >= 1) {
|
||||
} else if (prog.progress >= 1 && hasShownProgress) {
|
||||
// Only show "download complete" if we actually showed progress
|
||||
toast.loading("Loading model...", {
|
||||
id: toastId,
|
||||
description: "Download complete. Loading into memory...",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue