diff --git a/studio/backend/hub/services/models/cache_inventory.py b/studio/backend/hub/services/models/cache_inventory.py index 3e3754483e..d16a2aff1f 100644 --- a/studio/backend/hub/services/models/cache_inventory.py +++ b/studio/backend/hub/services/models/cache_inventory.py @@ -31,6 +31,7 @@ from hub.services.models.common import ( _is_checkpoint_weight_name, _is_gguf_filename, _is_main_gguf_filename, + _is_mmproj_filename, _is_transformers_safetensors_weight_name, _local_inventory_id, _prefer_complete_larger, @@ -143,6 +144,14 @@ def _repo_gguf_last_modified(repo_info) -> float: return latest +def _repo_has_mmproj(repo_info) -> bool: + return any( + _is_mmproj_filename(f.file_name) + for revision in repo_info.revisions + for f in revision.files + ) + + def _cached_repo_file_name(file_obj) -> str: file_path = getattr(file_obj, "file_path", None) if file_path: @@ -270,6 +279,8 @@ def _scan_cached_gguf() -> list[dict]: requires_variant = True, ) ) + if _repo_has_mmproj(repo_info): + row["capabilities"]["supports_vision"] = True if _prefer_cache_row(row, existing): seen_lower[key] = row elif last_modified > existing.get("last_modified", 0.0): diff --git a/studio/backend/picker/service.py b/studio/backend/picker/service.py index 4706196d2d..ae28dc3d6b 100644 --- a/studio/backend/picker/service.py +++ b/studio/backend/picker/service.py @@ -17,6 +17,7 @@ from hub.services.models.folder_browser import ( _build_browse_allowlist, _is_path_inside_allowlist, ) +from hub.utils.gguf import iter_hf_cache_snapshots from utils.models.gguf_metadata import read_gguf_chat_template from utils.models.model_config import ( _extract_quant_label, @@ -25,7 +26,6 @@ from utils.models.model_config import ( _is_mtp_drafter, ) from utils.paths.path_utils import ( - get_cache_path, is_local_path, normalize_path, resolve_cached_repo_id_case, @@ -210,18 +210,6 @@ def _chat_template_from_dir(dir_path: Path, gguf_variant: Optional[str] = None) return _chat_template_from_tokenizer_dir(dir_path) or from_gguf() -def _snapshots_newest_first(snapshots_dir: Path) -> list[Path]: - dirs_with_mtime: list[tuple[float, Path]] = [] - for entry in snapshots_dir.iterdir(): - try: - if entry.is_dir(): - dirs_with_mtime.append((entry.stat().st_mtime, entry)) - except OSError: - continue - dirs_with_mtime.sort(key = lambda item: item[0], reverse = True) - return [entry for _, entry in dirs_with_mtime] - - def read_default_chat_template( model_name: str, hf_token: Optional[str] = None, @@ -250,14 +238,10 @@ def read_default_chat_template( resolved = resolve_cached_repo_id_case(name) try: - repo_dir = get_cache_path(resolved) - if repo_dir is not None and repo_dir.exists(): - snapshots_dir = repo_dir / "snapshots" - if snapshots_dir.exists(): - for snapshot in _snapshots_newest_first(snapshots_dir): - template = _chat_template_from_dir(snapshot, gguf_variant) - if template: - return template + for snapshot in iter_hf_cache_snapshots(resolved): + template = _chat_template_from_dir(snapshot, gguf_variant) + if template: + return template except Exception as exc: logger.debug("Could not read cached chat template for %s: %s", resolved, exc) diff --git a/studio/backend/tests/test_picker_service.py b/studio/backend/tests/test_picker_service.py index fada4de3b4..8aab0b3eed 100644 --- a/studio/backend/tests/test_picker_service.py +++ b/studio/backend/tests/test_picker_service.py @@ -1,7 +1,16 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -from picker.service import _find_gguf_in_dir, _iter_ggufs +import json + +from picker.service import ( + _chat_template_from_dir, + _chat_template_from_tokenizer_config, + _chat_template_from_tokenizer_dir, + _find_gguf_in_dir, + _iter_ggufs, + validate_chat_template, +) def test_iter_ggufs_skips_gguf_companions(tmp_path): @@ -46,3 +55,68 @@ def test_find_gguf_in_dir_matches_bpw_variant_base_label(tmp_path): assert _find_gguf_in_dir(tmp_path, "IQ4_XS") == target assert _find_gguf_in_dir(tmp_path, "IQ4_XS-3.53bpw") == target assert _find_gguf_in_dir(tmp_path, "Q4_K") is None + + +def test_validate_chat_template_accepts_valid_and_empty(): + assert validate_chat_template("{{ messages[0].content }}").valid is True + assert validate_chat_template("").valid is True + assert validate_chat_template(" ").valid is True + + +def test_validate_chat_template_reports_syntax_error_with_line(): + result = validate_chat_template("{% if %}{% endif %}") + assert result.valid is False + assert result.error is not None + assert result.error.startswith("Line ") + + +def test_chat_template_from_tokenizer_config_reads_string(): + assert _chat_template_from_tokenizer_config({"chat_template": "HELLO"}) == "HELLO" + assert _chat_template_from_tokenizer_config({"chat_template": " "}) is None + assert _chat_template_from_tokenizer_config({}) is None + + +def test_chat_template_from_tokenizer_config_prefers_named_default(): + config = { + "chat_template": [ + {"name": "tool_use", "template": "TOOL"}, + {"name": "default", "template": "DEFAULT"}, + ] + } + assert _chat_template_from_tokenizer_config(config) == "DEFAULT" + + +def test_chat_template_from_tokenizer_config_falls_back_to_first_entry(): + config = { + "chat_template": [ + {"name": "tool_use", "template": "TOOL"}, + {"name": "other", "template": "OTHER"}, + ] + } + assert _chat_template_from_tokenizer_config(config) == "TOOL" + + +def test_chat_template_from_tokenizer_dir_prefers_jinja_file(tmp_path): + (tmp_path / "chat_template.jinja").write_text("FROM_JINJA", encoding="utf-8") + (tmp_path / "tokenizer_config.json").write_text( + json.dumps({"chat_template": "FROM_CONFIG"}), encoding="utf-8" + ) + assert _chat_template_from_tokenizer_dir(tmp_path) == "FROM_JINJA" + + +def test_chat_template_from_tokenizer_dir_reads_tokenizer_config(tmp_path): + (tmp_path / "tokenizer_config.json").write_text( + json.dumps({"chat_template": "FROM_CONFIG"}), encoding="utf-8" + ) + assert _chat_template_from_tokenizer_dir(tmp_path) == "FROM_CONFIG" + + +def test_chat_template_from_dir_without_variant_prefers_tokenizer(tmp_path): + (tmp_path / "tokenizer_config.json").write_text( + json.dumps({"chat_template": "FROM_CONFIG"}), encoding="utf-8" + ) + assert _chat_template_from_dir(tmp_path) == "FROM_CONFIG" + + +def test_chat_template_from_dir_returns_none_when_absent(tmp_path): + assert _chat_template_from_dir(tmp_path) is None diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index 8f3793e308..694158b458 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -1441,7 +1441,7 @@ async function autoLoadSmallestModel(): Promise<{ ggufContextLength: null, currentCheckpoint: currentStore.params.checkpoint, activeGgufVariant: currentStore.activeGgufVariant, - maxSeqLength: candidate.maxSeqLength, + maxSeqLength: config.maxSeqLength ?? candidate.maxSeqLength, presetSource: currentStore.activePresetSource, }); const effectiveSpeculativeType = @@ -1489,6 +1489,9 @@ async function autoLoadSmallestModel(): Promise<{ ); store.setParams({ ...store.params, + ...(candidate.kind === "gguf" + ? {} + : { maxSeqLength: effectiveMaxSeqLength }), maxTokens: candidate.kind === "gguf" ? loadResp.context_length ?? 131072 diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 8e79b32f27..80743020c0 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -11,7 +11,6 @@ import { ModelSelector, type ModelSelectorChangeMeta, type PerModelConfig, - perModelConfigsEqual, resolveInitialConfig, SidebarModelConfig, } from "@/features/model-picker"; @@ -1887,20 +1886,7 @@ export function ChatPage({ const isSameLoadedModel = value === currentCheckpoint && (meta?.ggufVariant ?? null) === (currentVariant ?? null); - const metaIsGguf = - meta?.isGguf === true || - meta?.ggufVariant != null || - value.toLowerCase().endsWith(".gguf"); - if ( - isSameLoadedModel && - (!meta?.config || - perModelConfigsEqual( - meta.config, - currentRuntimePerModelConfig({ - includeMaxSeqLength: !metaIsGguf, - }), - )) - ) { + if (isSameLoadedModel && !meta?.forceReload) { return; } if (meta?.source === "external" || isExternalModelId(value)) { @@ -2103,6 +2089,7 @@ export function ChatPage({ isGguf: activeModelIsGguf, isDownloaded: true, config, + forceReload: true, }); }, [ @@ -2731,7 +2718,7 @@ export function ChatPage({ params={inferenceParams} onParamsChange={setInferenceParams} modelConfig={ - view.mode !== "compare" && activeModelConfig ? ( + view.mode !== "compare" && activeModelConfig && !modelLoading ? ( (response: Response): Promise { - if (!response.ok) { - throw new Error(await readFastApiError(response)); - } - return response.json(); -} +import { getModelConfig } from "@/features/training"; export async function fetchModelMaxPositionEmbeddings( modelName: string, hfToken?: string | null, signal?: AbortSignal, ): Promise { - const query = hfToken?.trim() - ? `?hf_token=${encodeURIComponent(hfToken.trim())}` - : ""; - const response = await authFetch( - `/api/models/config/${encodeURIComponent(modelName)}${query}`, - { signal }, + const config = await getModelConfig( + modelName, + signal, + hfToken?.trim() || undefined, ); - const data = await parseJsonOrThrow<{ max_position_embeddings?: unknown }>( - response, - ); - const value = data.max_position_embeddings; + const value = config.max_position_embeddings; return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : null; diff --git a/studio/frontend/src/features/model-picker/components/model-config-page.tsx b/studio/frontend/src/features/model-picker/components/model-config-page.tsx index a2a3814a46..b1285164e1 100644 --- a/studio/frontend/src/features/model-picker/components/model-config-page.tsx +++ b/studio/frontend/src/features/model-picker/components/model-config-page.tsx @@ -30,11 +30,13 @@ import { import { perModelConfigsEqual } from "../model-config/apply-per-model-config"; import { DEFAULT_PER_MODEL_CONFIG, + KV_CACHE_DTYPES, MAX_SEQ_LENGTH_MAX, MAX_SEQ_LENGTH_MIN, MAX_SEQ_LENGTH_STEP, MTP_SPECULATIVE_TYPES, type PerModelConfig, + SPECULATIVE_TYPES, deletePerModelConfig, isDefaultConfig, normalizeMaxSeqLength, @@ -53,6 +55,16 @@ const CONTROL_SURFACE = const SELECT_TRIGGER_CLASS = `grid h-8 min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1 ${CONTROL_SURFACE} pl-3 pr-2 py-0 text-[13px]! font-medium text-nav-fg focus-visible:ring-0 focus-visible:border-transparent [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate [&>svg]:shrink-0`; const NUMBER_INPUT_CLASS = `h-8 w-[92px] ${CONTROL_SURFACE} pl-3 pr-2 py-0 text-right text-[13px] font-medium text-nav-fg outline-none focus-visible:ring-0`; +const KV_CACHE_DTYPE_DEFAULT = "f16"; +const SPECULATIVE_TYPE_LABELS: Record<(typeof SPECULATIVE_TYPES)[number], string> = + { + auto: "Auto", + mtp: "MTP", + ngram: "Ngram", + "mtp+ngram": "MTP+Ngram", + off: "Off", + }; + function hasNonDefaultAdvanced(config: PerModelConfig): boolean { return ( config.kvCacheDtype != null || @@ -182,9 +194,9 @@ function GgufAdvancedSettings({ @@ -232,11 +247,11 @@ function GgufAdvancedSettings({ - Auto - MTP - Ngram - MTP+Ngram - Off + {SPECULATIVE_TYPES.map((type) => ( + + {SPECULATIVE_TYPE_LABELS[type]} + + ))} @@ -317,6 +332,12 @@ export function ModelConfigPage({ const isActiveModel = loadedConfig != null; const runtimeMaxSeqLength = useChatRuntimeStore((s) => s.params.maxSeqLength); const hfToken = useChatRuntimeStore((s) => s.hfToken); + const loadedDefaultChatTemplate = useChatRuntimeStore( + (s) => s.defaultChatTemplate, + ); + const loadedMaxContextLength = useChatRuntimeStore( + (s) => s.ggufMaxContextLength, + ); const [initialMaxSeqLength] = useState( () => normalizeMaxSeqLength(runtimeMaxSeqLength) ?? 4096, ); @@ -353,6 +374,14 @@ export function ModelConfigPage({ target.id, !target.isGguf, ); + const hasLoadedDefaultTemplate = + isActiveModel && loadedDefaultChatTemplate != null; + const resolvedDefaultTemplate = hasLoadedDefaultTemplate + ? loadedDefaultChatTemplate + : templateDefaults.template; + const resolvedDefaultLoading = hasLoadedDefaultTemplate + ? false + : templateDefaults.loading; const update = (patch: Partial) => setConfig((current) => ({ ...current, ...patch })); @@ -454,7 +483,7 @@ export function ModelConfigPage({ customContextLength: contextBaseline == null && config.customContextLength == null ? null - : resolveCustomContextLength(contextValue, nativeContextLength), + : resolveCustomContextLength(contextValue, contextBaseline), } : { ...config, @@ -472,20 +501,21 @@ export function ModelConfigPage({ const handleRun = () => { const defaultConfig = isDefaultConfig(runtimeConfig); + let saveFailed = false; if (remember) { - const saved = savePerModelConfig( + saveFailed = !savePerModelConfig( target.id, target.ggufVariant, runtimeConfig, ); - if (!saved) { - toast.error("Couldn't save settings for this model."); - return; - } } else { deletePerModelConfig(target.id, target.ggufVariant); } if (persistenceOnly) { + if (saveFailed) { + toast.error("Couldn't save settings for this model."); + return; + } const nextRemember = remember && !defaultConfig; setSavedRemember(nextRemember); setRemember(nextRemember); @@ -498,6 +528,9 @@ export function ModelConfigPage({ ); return; } + if (saveFailed) { + toast.error("Couldn't save these settings, loading with them anyway."); + } onRun(runtimeConfig); }; @@ -566,6 +599,15 @@ export function ModelConfigPage({ aria-label="Context Length" /> ) : null} + {isActiveModel && + loadedMaxContextLength != null && + contextValue > loadedMaxContextLength && ( +

+ Exceeds estimated VRAM capacity ( + {loadedMaxContextLength.toLocaleString()} tokens). The model + may use system RAM. +

+ )} {showAdvanced && ( @@ -679,8 +721,8 @@ export function ModelConfigPage({ open={templateOpen} onOpenChange={setTemplateOpen} value={config.chatTemplateOverride} - defaultTemplate={templateDefaults.template} - defaultLoading={templateDefaults.loading} + defaultTemplate={resolvedDefaultTemplate} + defaultLoading={resolvedDefaultLoading} readOnly={!target.isGguf} onSave={(override) => update({ chatTemplateOverride: override })} /> diff --git a/studio/frontend/src/features/model-picker/components/model-selector.tsx b/studio/frontend/src/features/model-picker/components/model-selector.tsx index e89d70e45a..526f3566e7 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector.tsx @@ -542,6 +542,7 @@ function ModelSelectorContent({ onSelect(visibleConfigTarget.id, { ...visibleConfigTarget.meta, config, + forceReload: true, }) } loadedConfig={ diff --git a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx index de619f82e2..d75fc17022 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx @@ -1477,7 +1477,8 @@ export function HubModelPicker({ }, []); const pickerInventory = useChatPickerInventory({ enabled: true }); - const { cachedGguf, cachedModels, cachedReady } = pickerInventory; + const { cachedGguf, cachedModels, cachedReady, refreshInventory } = + pickerInventory; const lmStudioModels = useMemo( () => sortLmStudio( @@ -1658,6 +1659,10 @@ export function HubModelPicker({ .catch(() => {}); }, [refreshScanFolders]); + useEffect(() => { + void refreshInventory(); + }, [refreshInventory]); + // Hide downloaded models from the recommended list. Case-insensitive // since the HF cache lowercases repo IDs. const downloadedSet = useMemo(() => { diff --git a/studio/frontend/src/features/model-picker/components/model-selector/types.ts b/studio/frontend/src/features/model-picker/components/model-selector/types.ts index dab7e3df22..2967f0ce51 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector/types.ts +++ b/studio/frontend/src/features/model-picker/components/model-selector/types.ts @@ -38,6 +38,7 @@ export interface ModelSelectorChangeMeta { * Studio). Marks it as a GGUF source for the deferred-load staging flow. */ isGguf?: boolean; config?: PerModelConfig; + forceReload?: boolean; } export interface ModelPickTarget { diff --git a/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts b/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts index 8cec99e942..d40687e468 100644 --- a/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts +++ b/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import { looksLikeLocalPath, useHfTokenStore } from "@/features/hub"; +import { useHfTokenStore } from "@/features/hub"; import { useEffect, useState } from "react"; import { fetchModelMaxPositionEmbeddings } from "../api/model-metadata"; import { fetchDefaultChatTemplate } from "../api/templates"; @@ -69,7 +69,7 @@ export function useDefaultChatTemplate( if (controller.signal.aborted) { return; } - if (!(template === null && looksLikeLocalPath(modelId))) { + if (template !== null) { cacheTemplate(cacheKey, template); } setFetched({ diff --git a/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts b/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts index 14a54d45e0..50b6bbb1a6 100644 --- a/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts +++ b/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts @@ -10,6 +10,7 @@ import { type CachedInventoryRow, type LocalInventoryRow, type LocalSource, + isHiddenModelId, useHubInventory, } from "@/features/hub"; import { useMemo } from "react"; @@ -74,21 +75,35 @@ export function useChatPickerInventory( const cachedGguf = useMemo( () => inventory.cachedRows - .filter((row) => row.modelFormat === "gguf" && isCompleteCachedRow(row)) + .filter( + (row) => + row.modelFormat === "gguf" && + isCompleteCachedRow(row) && + !isHiddenModelId(row.repoId), + ) .map(toCachedGgufRepo), [inventory.cachedRows], ); const cachedModels = useMemo( () => inventory.cachedRows - .filter((row) => row.modelFormat !== "gguf" && isCompleteCachedRow(row)) + .filter( + (row) => + row.modelFormat !== "gguf" && + isCompleteCachedRow(row) && + !isHiddenModelId(row.repoId), + ) .map(toCachedModelRepo), [inventory.cachedRows], ); const localModels = useMemo( () => inventory.localRows - .filter((row) => PICKER_LOCAL_SOURCES.has(row.source)) + .filter( + (row) => + PICKER_LOCAL_SOURCES.has(row.source) && + !isHiddenModelId(row.modelId, row.repoId, row.path), + ) .map(toLocalModelInfo), [inventory.localRows], ); diff --git a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts index e21233251a..9c2f298fc1 100644 --- a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts +++ b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts @@ -18,10 +18,11 @@ function cleanTemplate(value: string | null | undefined): string | null { export function applyPerModelConfigToRuntime(config: PerModelConfig): void { const maxSeqLength = normalizeMaxSeqLength(config.maxSeqLength); - useChatRuntimeStore.setState((state) => ({ - ...(maxSeqLength == null - ? {} - : { params: { ...state.params, maxSeqLength } }), + const store = useChatRuntimeStore.getState(); + if (maxSeqLength != null && maxSeqLength !== store.params.maxSeqLength) { + store.setParams({ ...store.params, maxSeqLength }); + } + useChatRuntimeStore.setState({ customContextLength: config.customContextLength ?? null, kvCacheDtype: config.kvCacheDtype ?? null, speculativeType: @@ -30,7 +31,7 @@ export function applyPerModelConfigToRuntime(config: PerModelConfig): void { specDraftNMax: config.specDraftNMax ?? null, tensorParallel: config.tensorParallel ?? false, chatTemplateOverride: cleanTemplate(config.chatTemplateOverride), - })); + }); } export function applyModelLoadConfigToRuntime( diff --git a/studio/frontend/src/features/model-picker/model-config/per-model-config.ts b/studio/frontend/src/features/model-picker/model-config/per-model-config.ts index e912f92a7c..b744208d00 100644 --- a/studio/frontend/src/features/model-picker/model-config/per-model-config.ts +++ b/studio/frontend/src/features/model-picker/model-config/per-model-config.ts @@ -53,7 +53,7 @@ const LEGACY_STORAGE_KEY = "unsloth_load_settings"; const LEGACY_MIGRATION_FLAG = "unsloth_model_configs_migrated"; const STORAGE_SCHEMA_VERSION = 1; const MAX_ENTRIES = 500; -export const MAX_PER_MODEL_CONFIG_STORAGE_BYTES = 1024 * 1024; +const MAX_PER_MODEL_CONFIG_STORAGE_BYTES = 1024 * 1024; export const MAX_CHAT_TEMPLATE_BYTES = 65_536; type StoredPerModelConfig = PerModelConfig & { @@ -149,17 +149,6 @@ function deleteOldestEvictableEntry( return null; } -function isMostRecentEntry(map: StoredMap, key: string): boolean { - const keys = Object.keys(map); - return keys.length > 0 && keys[keys.length - 1] === key; -} - -function touchEntry(map: StoredMap, key: string): void { - const value = map[key]; - delete map[key]; - map[key] = value; -} - function enforceStorageBudget(map: StoredMap, protectedKey?: string): boolean { let entryCount = Object.keys(map).length; while (entryCount > MAX_ENTRIES) { @@ -255,9 +244,12 @@ function migrateLegacyLoadSettingsOnce(): void { if (localStorage.getItem(LEGACY_MIGRATION_FLAG)) { return; } - const legacy = JSON.parse( - localStorage.getItem(LEGACY_STORAGE_KEY) ?? "null", - ); + let legacy: unknown = null; + try { + legacy = JSON.parse(localStorage.getItem(LEGACY_STORAGE_KEY) ?? "null"); + } catch { + legacy = null; + } if (!legacy || typeof legacy !== "object" || Array.isArray(legacy)) { localStorage.setItem(LEGACY_MIGRATION_FLAG, "1"); return; @@ -270,8 +262,6 @@ function migrateLegacyLoadSettingsOnce(): void { enforceStorageBudget(map); if (writeMap(map)) { localStorage.setItem(LEGACY_MIGRATION_FLAG, "1"); - } else { - legacyMigrationChecked = false; } } catch (err) { console.warn("Failed to migrate legacy load settings:", err); @@ -472,36 +462,13 @@ function deleteConfigEntriesForModelVariant( return changed; } -function loadPerModelConfigInternal( +function loadPerModelConfig( modelId: string, - ggufVariant: string | null | undefined, - touch: boolean, + ggufVariant?: string | null, ): PerModelConfig | null { const map = readMap(); const key = findConfigKeyForModelVariant(map, modelId, ggufVariant); - if (!key) { - return null; - } - const config = normalize(map[key]); - if (touch && !isMostRecentEntry(map, key)) { - touchEntry(map, key); - writeMap(map); - } - return config; -} - -export function loadPerModelConfig( - modelId: string, - ggufVariant?: string | null, -): PerModelConfig | null { - return loadPerModelConfigInternal(modelId, ggufVariant, true); -} - -export function hasPerModelConfig( - modelId: string, - ggufVariant?: string | null, -): boolean { - return loadPerModelConfigInternal(modelId, ggufVariant, false) != null; + return key ? normalize(map[key]) : null; } export function isDefaultConfig(config: PerModelConfig): boolean { diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts index 553dcc2af5..fcb30f379c 100644 --- a/studio/frontend/src/features/training/index.ts +++ b/studio/frontend/src/features/training/index.ts @@ -24,8 +24,8 @@ export { HfDatasetSubsetSplitSelectors } from "./components/hf-dataset-subset-sp export { useDatasetPreviewDialogStore } from "./stores/dataset-preview-dialog-store"; export { listLocalDatasets, uploadTrainingDataset } from "./api/datasets-api"; export type { LocalDatasetInfo } from "./types/datasets"; -export { listLocalModels } from "./api/models-api"; -export type { LocalModelInfo } from "./api/models-api"; +export { getModelConfig, listLocalModels } from "./api/models-api"; +export type { LocalModelInfo, ModelConfigResponse } from "./api/models-api"; export type { TrainingPhase, TrainingViewData,