diff --git a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx index e40f5fd5ac..3c020a7199 100644 --- a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx +++ b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx @@ -4,6 +4,7 @@ import { TrainIcon } from "../components/train-icon"; import { HUB_GGUF_RUN_ACTIONS_VISIBLE, + HUB_NON_GGUF_RUN_ACTIONS_VISIBLE, HUB_POST_DOWNLOAD_ACTIONS_VISIBLE, } from "../lib/hub-feature-flags"; import { @@ -410,7 +411,7 @@ export function LocalOnDeviceCard({ const showOldCacheHint = source === "hf_cache" && !!unsupportedReason; const runActionsVisible = isGguf ? HUB_GGUF_RUN_ACTIONS_VISIBLE - : HUB_POST_DOWNLOAD_ACTIONS_VISIBLE; + : HUB_NON_GGUF_RUN_ACTIONS_VISIBLE; return (
diff --git a/studio/frontend/src/features/hub/catalog/model-inspector.tsx b/studio/frontend/src/features/hub/catalog/model-inspector.tsx index 0f244bce53..a67b2a57cc 100644 --- a/studio/frontend/src/features/hub/catalog/model-inspector.tsx +++ b/studio/frontend/src/features/hub/catalog/model-inspector.tsx @@ -263,17 +263,22 @@ type VramInfo = { est: number; status: "fits" | "tight" | "exceeds" } | null; function ModelStatusChips({ isDataset, isGguf, + chatOnly, unslothSupport, vramInfo, }: { isDataset: boolean; isGguf: boolean; + chatOnly: boolean; unslothSupport: UnslothSupport; vramInfo: VramInfo; }) { const showUnsupported = !isDataset && unslothSupport.status === "unsupported"; + // The format-unsupported chip already explains itself; this one covers the + // supported-format model a chat-only host still can't run. + const showChatOnly = !isDataset && !isGguf && chatOnly && !showUnsupported; const showVram = !isDataset && vramInfo && !isGguf; - if (!showUnsupported && !showVram) return null; + if (!showUnsupported && !showChatOnly && !showVram) return null; const vramTone = vramInfo ? vramInfo.status === "exceeds" @@ -323,6 +328,26 @@ function ModelStatusChips({ )} + {showChatOnly && ( + + + + + + + + This device has no supported GPU or usable MLX, so only GGUF models + can run here. + + Still downloadable to your Hugging Face cache. + + + + )} {showVram && vramInfo && ( @@ -406,6 +431,7 @@ export const ModelInspector = memo(function ModelInspector({ onSearchHub, } = actions; const deviceType = usePlatformStore((s) => s.deviceType); + const chatOnly = usePlatformStore((s) => s.isChatOnly()); const hfToken = useHfTokenStore((s) => s.token); const datasetRepoId = isDataset && model?.hubRepoId ? model.hubRepoId : null; const datasetSize = useDatasetSize(datasetRepoId, { @@ -504,15 +530,19 @@ export const ModelInspector = memo(function ModelInspector({ const paramsLabel = model.totalParams ? formatCompact(model.totalParams) : "N/A"; - const trainingSupported = unslothSupport.status !== "unsupported"; + const unslothSupported = unslothSupport.status !== "unsupported"; + // Chat-only hosts (no supported GPU / usable MLX) run inference only through + // llama.cpp, so only GGUF is loadable. const canRunModel = - !isDataset && (model.runtimeCapabilities?.canChat ?? true); + !isDataset && + (model.runtimeCapabilities?.canChat ?? true) && + (model.isGguf || (!chatOnly && unslothSupported)); const canTrainModel = !isDataset && (model.runtimeCapabilities?.canTrain ?? false) && model.modelFormat !== "gguf" && model.modelFormat !== "adapter" && - trainingSupported; + unslothSupported; const languages = parseLanguageTags(model.tags); const datasetSizeBytes = @@ -765,6 +795,7 @@ export const ModelInspector = memo(function ModelInspector({ diff --git a/studio/frontend/src/features/hub/catalog/safetensors-download-card.tsx b/studio/frontend/src/features/hub/catalog/safetensors-download-card.tsx index ccd61c5f04..424afa2e05 100644 --- a/studio/frontend/src/features/hub/catalog/safetensors-download-card.tsx +++ b/studio/frontend/src/features/hub/catalog/safetensors-download-card.tsx @@ -16,7 +16,10 @@ import { PlayIcon, } from "@hugeicons/core-free-icons"; import { TrainIcon } from "../components/train-icon"; -import { HUB_POST_DOWNLOAD_ACTIONS_VISIBLE } from "../lib/hub-feature-flags"; +import { + HUB_NON_GGUF_RUN_ACTIONS_VISIBLE, + HUB_POST_DOWNLOAD_ACTIONS_VISIBLE, +} from "../lib/hub-feature-flags"; import { HugeiconsIcon } from "@hugeicons/react"; import { useEffect, useState } from "react"; import { useHfTokenStore } from "../stores/hf-token-store"; @@ -158,6 +161,7 @@ export function SafetensorsDownloadCard({ const showActionPair = isDownloaded && !downloading && (canRun || !!onTrain); const showUnavailableAction = isDownloaded && !downloading && !canRun && !onTrain; + const trainActionVisible = !!onTrain && HUB_POST_DOWNLOAD_ACTIONS_VISIBLE; const canDelete = (isDownloaded || isPartial) && !downloading && @@ -238,17 +242,17 @@ export function SafetensorsDownloadCard({ )}
- {/* Divider sits above the Download CTA; in the action-pair state it hides with the pair. */} - {(!showActionPair || HUB_POST_DOWNLOAD_ACTIONS_VISIBLE) && } + {/* Info/actions hairline; dropped for the run action row (no divider before + Run, as in the GGUF card's Run CTA), restored when the Train pair ships. */} + {(!showActionPair || trainActionVisible) && } {showActionPair ? (