rename version -> backend and backend -> acceleration, display "metal" for mac users and don't show other options, allow "metal" users to select GPU layers

This commit is contained in:
lone-cloud 2025-11-28 23:33:37 -08:00
parent 3c2d987769
commit 72d4aacb5a
28 changed files with 677 additions and 652 deletions

View file

@ -60,7 +60,7 @@
"eslint-plugin-sonarjs": "^3.0.5", "eslint-plugin-sonarjs": "^3.0.5",
"globals": "^16.5.0", "globals": "^16.5.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"prettier": "^3.7.1", "prettier": "^3.7.2",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.4" "vite": "^7.2.4"

View file

@ -35,7 +35,7 @@ export const UpdateAvailableModal = ({
onUpdate, onUpdate,
}: UpdateAvailableModalProps) => { }: UpdateAvailableModalProps) => {
const { downloading, downloadProgress } = useKoboldVersionsStore(); const { downloading, downloadProgress } = useKoboldVersionsStore();
const currentVersion = updateInfo?.currentVersion; const currentBackend = updateInfo?.currentBackend;
const availableUpdate = updateInfo?.availableUpdate; const availableUpdate = updateInfo?.availableUpdate;
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
@ -73,7 +73,7 @@ export const UpdateAvailableModal = ({
Current Version Current Version
</Text> </Text>
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{currentVersion?.version} {currentBackend?.version}
</Text> </Text>
</div> </div>

View file

@ -98,12 +98,12 @@ export const App = () => {
useEffect(() => { useEffect(() => {
const checkInstallation = async () => { const checkInstallation = async () => {
const [currentVersion, hasSeenWelcome] = await Promise.all([ const [currentBackend, hasSeenWelcome] = await Promise.all([
window.electronAPI.kobold.getCurrentVersion(), window.electronAPI.kobold.getCurrentBackend(),
window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>, window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>,
]); ]);
determineScreen(currentVersion, hasSeenWelcome); determineScreen(currentBackend, hasSeenWelcome);
setHasInitialized(true); setHasInitialized(true);
}; };
@ -114,9 +114,9 @@ export const App = () => {
const runUpdateCheck = async () => { const runUpdateCheck = async () => {
if (loadingRemote || !hasInitialized) return; if (loadingRemote || !hasInitialized) return;
const currentVersion = const currentBackend =
await window.electronAPI.kobold.getCurrentVersion(); await window.electronAPI.kobold.getCurrentBackend();
if (currentVersion) { if (currentBackend) {
setTimeout(() => { setTimeout(() => {
checkForUpdates(); checkForUpdates();
}, 5000); }, 5000);
@ -136,13 +136,13 @@ export const App = () => {
}, [loadingRemote, hasInitialized, checkForUpdates]); }, [loadingRemote, hasInitialized, checkForUpdates]);
const handleBinaryUpdate = async (download: DownloadItem) => { const handleBinaryUpdate = async (download: DownloadItem) => {
const currentVersion = await window.electronAPI.kobold.getCurrentVersion(); const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
await handleDownload({ await handleDownload({
item: download, item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: true, wasCurrentBinary: true,
oldVersionPath: currentVersion?.path, oldVersionPath: currentBackend?.path,
}); });
closeModal(); closeModal();
@ -170,8 +170,8 @@ export const App = () => {
const handleWelcomeComplete = async () => { const handleWelcomeComplete = async () => {
window.electronAPI.config.set('hasSeenWelcome', true); window.electronAPI.config.set('hasSeenWelcome', true);
const currentVersion = await window.electronAPI.kobold.getCurrentVersion(); const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
determineScreen(currentVersion, true); determineScreen(currentBackend, true);
}; };
const handleDownloadComplete = () => setCurrentScreen('launch'); const handleDownloadComplete = () => setCurrentScreen('launch');

View file

@ -59,7 +59,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<Stack gap="xl"> <Stack gap="xl">
<Card withBorder radius="md" shadow="sm"> <Card withBorder radius="md" shadow="sm">
<Stack gap="lg"> <Stack gap="lg">
<Title order={3}>Available Binaries for Your Platform</Title> <Title order={3}>Select a Backend</Title>
{loading ? ( {loading ? (
<Stack align="center" gap="md" py="xl"> <Stack align="center" gap="md" py="xl">

View file

@ -61,8 +61,9 @@ export const AdvancedTab = () => {
const isGpuBackend = backend === 'cuda' || backend === 'rocm'; const isGpuBackend = backend === 'cuda' || backend === 'rocm';
useEffect(() => { useEffect(() => {
const detectBackendSupport = async () => { const detectAccelerationSupport = async () => {
const support = await window.electronAPI.kobold.detectBackendSupport(); const support =
await window.electronAPI.kobold.detectAccelerationSupport();
if (support) { if (support) {
setBackendSupport({ setBackendSupport({
@ -76,7 +77,7 @@ export const AdvancedTab = () => {
setIsLoading(false); setIsLoading(false);
}; };
void detectBackendSupport(); void detectAccelerationSupport();
}, []); }, []);
return ( return (

View file

@ -1,14 +1,14 @@
import { Text, Group, Badge, Box } from '@mantine/core'; import { Text, Group, Badge, Box } from '@mantine/core';
import type { BackendOption } from '@/types'; import type { AccelerationOption } from '@/types';
import { GPUDevice } from '@/types/hardware'; import { GPUDevice } from '@/types/hardware';
type BackendSelectItemProps = Omit<BackendOption, 'value'>; type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
export const BackendSelectItem = ({ export const AccelerationSelectItem = ({
label, label,
devices, devices,
disabled = false, disabled = false,
}: BackendSelectItemProps) => { }: AccelerationSelectItemProps) => {
const renderDeviceName = (device: string | GPUDevice) => { const renderDeviceName = (device: string | GPUDevice) => {
const deviceName = typeof device === 'string' ? device : device.name; const deviceName = typeof device === 'string' ? device : device.name;
return deviceName.length > 25 return deviceName.length > 25

View file

@ -1,13 +1,13 @@
import { Text, Group, Checkbox, TextInput } from '@mantine/core'; import { Text, Group, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem'; import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector'; import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import type { BackendOption } from '@/types'; import type { AccelerationOption } from '@/types';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
export const BackendSelector = () => { export const AccelerationSelector = () => {
const { const {
backend, backend,
gpuLayers, gpuLayers,
@ -21,60 +21,67 @@ export const BackendSelector = () => {
handleAutoGpuLayersChange, handleAutoGpuLayersChange,
} = useLaunchConfig(); } = useLaunchConfig();
const [availableBackends, setAvailableBackends] = useState<BackendOption[]>( const [availableAccelerations, setAvailableAccelerations] = useState<
[] AccelerationOption[]
); >([]);
const [isLoadingBackends, setIsLoadingBackends] = useState(false); const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false);
const [isCalculatingLayers, setIsCalculatingLayers] = useState(false); const [isCalculatingLayers, setIsCalculatingLayers] = useState(false);
const [isMac, setIsMac] = useState(false);
const hasInitialized = useRef(false); const hasInitialized = useRef(false);
useEffect(() => { useEffect(() => {
const loadBackends = async () => { const loadAccelerations = async () => {
setIsLoadingBackends(true); setIsLoadingAccelerations(true);
const backends = const [accelerations, platform] = await Promise.all([
await window.electronAPI.kobold.getAvailableBackends(true); window.electronAPI.kobold.getAvailableAccelerations(true),
window.electronAPI.kobold.getPlatform(),
]);
setAvailableBackends(backends || []); setAvailableAccelerations(accelerations || []);
setIsLoadingBackends(false); setIsMac(platform === 'darwin');
setIsLoadingAccelerations(false);
hasInitialized.current = true; hasInitialized.current = true;
}; };
if (!hasInitialized.current) { if (!hasInitialized.current) {
loadBackends(); loadAccelerations();
} }
const cleanup = window.electronAPI.kobold.onVersionsUpdated(() => { const cleanup = window.electronAPI.kobold.onVersionsUpdated(() => {
hasInitialized.current = false; hasInitialized.current = false;
loadBackends(); loadAccelerations();
}); });
return cleanup; return cleanup;
}, []); }, []);
useEffect(() => { useEffect(() => {
if (availableBackends.length > 0 && backend) { if (availableAccelerations.length > 0 && backend) {
const isBackendAvailable = availableBackends.some( const isAccelerationAvailable = availableAccelerations.some(
(b) => b.value === backend && !b.disabled (a) => a.value === backend && !a.disabled
); );
if (!isBackendAvailable) { if (!isAccelerationAvailable) {
const fallbackBackend = availableBackends.find((b) => !b.disabled); const fallbackAcceleration = availableAccelerations.find(
if (fallbackBackend) { (a) => !a.disabled
handleBackendChange(fallbackBackend.value); );
if (fallbackAcceleration) {
handleBackendChange(fallbackAcceleration.value);
} }
} }
} }
}, [availableBackends, backend, handleBackendChange]); }, [availableAccelerations, backend, handleBackendChange]);
useEffect(() => { useEffect(() => {
const calculateLayers = async () => { const calculateLayers = async () => {
const isCpuOnly = backend === 'cpu' && !isMac;
if ( if (
!autoGpuLayers || !autoGpuLayers ||
!model || !model ||
!contextSize || !contextSize ||
backend === 'cpu' || isCpuOnly ||
isLoadingBackends isLoadingAccelerations
) { ) {
return; return;
} }
@ -133,7 +140,8 @@ export const BackendSelector = () => {
backend, backend,
gpuDeviceSelection, gpuDeviceSelection,
flashattention, flashattention,
isLoadingBackends, isLoadingAccelerations,
isMac,
handleGpuLayersChange, handleGpuLayersChange,
]); ]);
@ -143,16 +151,20 @@ export const BackendSelector = () => {
<div style={{ flex: 1, marginRight: '1rem' }}> <div style={{ flex: 1, marginRight: '1rem' }}>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
Backend Acceleration
</Text> </Text>
<InfoTooltip label="Select a backend to use to run LLMs. CUDA runs on NVIDIA GPUs and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." /> <InfoTooltip label="Select an acceleration mode to run LLMs. CUDA runs on NVIDIA GPUs and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast work on all GPUs." />
</Group> </Group>
<Select <Select
placeholder={ placeholder={
isLoadingBackends ? 'Loading backends...' : 'Select backend' isLoadingAccelerations
? 'Loading accelerations...'
: 'Select acceleration'
} }
value={ value={
availableBackends.some((b) => b.value === backend && !b.disabled) availableAccelerations.some(
(a) => a.value === backend && !a.disabled
)
? backend ? backend
: null : null
} }
@ -161,22 +173,24 @@ export const BackendSelector = () => {
handleBackendChange(value); handleBackendChange(value);
} }
}} }}
data={availableBackends.map((b) => ({ data={availableAccelerations.map((a) => ({
value: b.value, value: a.value,
label: b.label, label: a.label,
disabled: b.disabled, disabled: a.disabled,
}))} }))}
disabled={isLoadingBackends || availableBackends.length === 0} disabled={
isLoadingAccelerations || availableAccelerations.length === 0
}
renderOption={({ option }) => { renderOption={({ option }) => {
const backendData = availableBackends.find( const accelerationData = availableAccelerations.find(
(b) => b.value === option.value (a) => a.value === option.value
); );
return ( return (
<BackendSelectItem <AccelerationSelectItem
label={backendData?.label || option.label.split(' (')[0]} label={accelerationData?.label || option.label.split(' (')[0]}
devices={backendData?.devices} devices={accelerationData?.devices}
disabled={backendData?.disabled} disabled={accelerationData?.disabled}
/> />
); );
}} }}
@ -209,7 +223,7 @@ export const BackendSelector = () => {
step={1} step={1}
size="sm" size="sm"
w={80} w={80}
disabled={autoGpuLayers || backend === 'cpu'} disabled={autoGpuLayers || (backend === 'cpu' && !isMac)}
/> />
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
@ -219,7 +233,7 @@ export const BackendSelector = () => {
handleAutoGpuLayersChange(event.currentTarget.checked) handleAutoGpuLayersChange(event.currentTarget.checked)
} }
size="sm" size="sm"
disabled={backend === 'cpu'} disabled={backend === 'cpu' && !isMac}
/> />
<InfoTooltip label="Automatically calculate optimal GPU layers based on available VRAM. The calculation accounts for model size, context size and flash attention." /> <InfoTooltip label="Automatically calculate optimal GPU layers based on available VRAM. The calculation accounts for model size, context size and flash attention." />
</Group> </Group>
@ -227,7 +241,7 @@ export const BackendSelector = () => {
</div> </div>
</Group> </Group>
<GpuDeviceSelector availableBackends={availableBackends} /> <GpuDeviceSelector availableAccelerations={availableAccelerations} />
</div> </div>
); );
}; };

View file

@ -2,14 +2,14 @@ import { Text, Group, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import type { BackendOption } from '@/types'; import type { AccelerationOption } from '@/types';
interface GpuDeviceSelectorProps { interface GpuDeviceSelectorProps {
availableBackends: BackendOption[]; availableAccelerations: AccelerationOption[];
} }
export const GpuDeviceSelector = ({ export const GpuDeviceSelector = ({
availableBackends, availableAccelerations,
}: GpuDeviceSelectorProps) => { }: GpuDeviceSelectorProps) => {
const { const {
backend, backend,
@ -19,21 +19,23 @@ export const GpuDeviceSelector = ({
handleTensorSplitChange, handleTensorSplitChange,
} = useLaunchConfig(); } = useLaunchConfig();
const selectedBackend = availableBackends.find((b) => b.value === backend); const selectedAcceleration = availableAccelerations.find(
const isGpuBackend = (a) => a.value === backend
);
const isGpuAcceleration =
backend === 'cuda' || backend === 'cuda' ||
backend === 'rocm' || backend === 'rocm' ||
backend === 'vulkan' || backend === 'vulkan' ||
backend === 'clblast'; backend === 'clblast';
const getDiscreteDeviceCount = () => { const getDiscreteDeviceCount = () => {
if (!selectedBackend?.devices) return 0; if (!selectedAcceleration?.devices) return 0;
if (backend === 'clblast' || backend === 'vulkan' || backend === 'rocm') { if (backend === 'clblast' || backend === 'vulkan' || backend === 'rocm') {
return selectedBackend.devices.filter( return selectedAcceleration.devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated (device) => typeof device === 'string' || !device.isIntegrated
).length; ).length;
} }
return selectedBackend.devices.length; return selectedAcceleration.devices.length;
}; };
const hasMultipleDevices = getDiscreteDeviceCount() > 1; const hasMultipleDevices = getDiscreteDeviceCount() > 1;
@ -42,15 +44,15 @@ export const GpuDeviceSelector = ({
hasMultipleDevices && hasMultipleDevices &&
gpuDeviceSelection === 'all'; gpuDeviceSelection === 'all';
if (!isGpuBackend || !hasMultipleDevices) { if (!isGpuAcceleration || !hasMultipleDevices) {
return null; return null;
} }
const deviceOptions = (() => { const deviceOptions = (() => {
if (!selectedBackend?.devices) return []; if (!selectedAcceleration?.devices) return [];
if (backend === 'clblast') { if (backend === 'clblast') {
return selectedBackend.devices return selectedAcceleration.devices
.map((device, index) => { .map((device, index) => {
if (typeof device === 'object' && device.isIntegrated) { if (typeof device === 'object' && device.isIntegrated) {
return null; return null;
@ -67,7 +69,7 @@ export const GpuDeviceSelector = ({
} }
if (backend === 'vulkan' || backend === 'rocm') { if (backend === 'vulkan' || backend === 'rocm') {
const discreteDeviceOptions = selectedBackend.devices const discreteDeviceOptions = selectedAcceleration.devices
.map((device, index) => { .map((device, index) => {
if (typeof device === 'object' && device.isIntegrated) { if (typeof device === 'object' && device.isIntegrated) {
return null; return null;
@ -87,7 +89,7 @@ export const GpuDeviceSelector = ({
return [ return [
{ value: 'all', label: 'All GPUs' }, { value: 'all', label: 'All GPUs' },
...selectedBackend.devices.map((device, index) => { ...selectedAcceleration.devices.map((device, index) => {
const deviceName = const deviceName =
typeof device === 'string' typeof device === 'string'
? device ? device

View file

@ -7,7 +7,7 @@ import {
Transition, Transition,
} from '@mantine/core'; } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector'; import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField'; import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
@ -33,7 +33,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
> >
{(styles) => ( {(styles) => (
<Stack gap="md" style={styles}> <Stack gap="md" style={styles}>
<BackendSelector /> <AccelerationSelector />
<ModelFileField <ModelFileField
label="Text Model File" label="Text Model File"

View file

@ -83,10 +83,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
}); });
const setHappyDefaults = useCallback(async () => { const setHappyDefaults = useCallback(async () => {
const backends = await window.electronAPI.kobold.getAvailableBackends(); const accelerations =
await window.electronAPI.kobold.getAvailableAccelerations();
if (!backend && backends && backends.length > 0) { if (!backend && accelerations && accelerations.length > 0) {
handleBackendChange(backends[0].value); handleBackendChange(accelerations[0].value);
} }
}, [backend, handleBackendChange]); }, [backend, handleBackendChange]);

View file

@ -19,10 +19,10 @@ import {
import { formatDownloadSize } from '@/utils/format'; import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersionsStore } from '@/stores/koboldVersions'; import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
import type { VersionInfo } from '@/types'; import type { VersionInfo } from '@/types';
export const VersionsTab = () => { export const BackendsTab = () => {
const { const {
availableDownloads, availableDownloads,
loadingPlatform, loadingPlatform,
@ -33,10 +33,10 @@ export const VersionsTab = () => {
initialize, initialize,
} = useKoboldVersionsStore(); } = useKoboldVersionsStore();
const [installedVersions, setInstalledVersions] = useState< const [installedBackends, setInstalledBackends] = useState<
InstalledVersion[] InstalledBackend[]
>([]); >([]);
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>( const [currentBackend, setCurrentBackend] = useState<InstalledBackend | null>(
null null
); );
const [loadingInstalled, setLoadingInstalled] = useState(true); const [loadingInstalled, setLoadingInstalled] = useState(true);
@ -45,16 +45,16 @@ export const VersionsTab = () => {
); );
const downloadingItemRef = useRef<HTMLDivElement>(null); const downloadingItemRef = useRef<HTMLDivElement>(null);
const loadInstalledVersions = useCallback(async () => { const loadInstalledBackends = useCallback(async () => {
setLoadingInstalled(true); setLoadingInstalled(true);
const [versions, currentVersion] = await Promise.all([ const [backends, current] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.kobold.getInstalledBackends(),
window.electronAPI.kobold.getCurrentVersion(), window.electronAPI.kobold.getCurrentBackend(),
]); ]);
setInstalledVersions(versions); setInstalledBackends(backends);
setCurrentVersion(currentVersion); setCurrentBackend(current);
setLoadingInstalled(false); setLoadingInstalled(false);
}, []); }, []);
@ -72,10 +72,10 @@ export const VersionsTab = () => {
} }
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
loadInstalledVersions(); loadInstalledBackends();
loadLatestRelease(); loadLatestRelease();
}, [ }, [
loadInstalledVersions, loadInstalledBackends,
loadLatestRelease, loadLatestRelease,
availableDownloads.length, availableDownloads.length,
loadingRemote, loadingRemote,
@ -83,47 +83,47 @@ export const VersionsTab = () => {
initialize, initialize,
]); ]);
const allVersions = useMemo((): VersionInfo[] => { const allBackends = useMemo((): VersionInfo[] => {
const versions: VersionInfo[] = []; const backends: VersionInfo[] = [];
const processedInstalled = new Set<string>(); const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => { availableDownloads.forEach((download) => {
const downloadBaseName = stripAssetExtensions(download.name); const downloadBaseName = stripAssetExtensions(download.name);
const installedVersion = installedVersions.find((v) => { const installedBackend = installedBackends.find((b) => {
const displayName = getDisplayNameFromPath(v); const displayName = getDisplayNameFromPath(b);
return displayName === downloadBaseName; return displayName === downloadBaseName;
}); });
const isCurrent = Boolean( const isCurrent = Boolean(
installedVersion && installedBackend &&
currentVersion && currentBackend &&
currentVersion.path === installedVersion.path currentBackend.path === installedBackend.path
); );
if (installedVersion) { if (installedBackend) {
processedInstalled.add(installedVersion.path); processedInstalled.add(installedBackend.path);
const hasUpdate = const hasUpdate =
compareVersions( compareVersions(
download.version || 'unknown', download.version || 'unknown',
installedVersion.version installedBackend.version
) > 0; ) > 0;
versions.push({ backends.push({
name: download.name, name: download.name,
version: installedVersion.version, version: installedBackend.version,
size: undefined, size: undefined,
isInstalled: true, isInstalled: true,
isCurrent, isCurrent,
downloadUrl: download.url, downloadUrl: download.url,
installedPath: installedVersion.path, installedPath: installedBackend.path,
hasUpdate, hasUpdate,
newerVersion: hasUpdate ? download.version : undefined, newerVersion: hasUpdate ? download.version : undefined,
actualVersion: installedVersion.actualVersion, actualVersion: installedBackend.actualVersion,
}); });
} else { } else {
versions.push({ backends.push({
name: download.name, name: download.name,
version: download.version || 'unknown', version: download.version || 'unknown',
size: download.size, size: download.size,
@ -134,14 +134,14 @@ export const VersionsTab = () => {
} }
}); });
installedVersions.forEach((installed) => { installedBackends.forEach((installed) => {
if (!processedInstalled.has(installed.path)) { if (!processedInstalled.has(installed.path)) {
const displayName = getDisplayNameFromPath(installed); const displayName = getDisplayNameFromPath(installed);
const isCurrent = Boolean( const isCurrent = Boolean(
currentVersion && currentVersion.path === installed.path currentBackend && currentBackend.path === installed.path
); );
versions.push({ backends.push({
name: displayName, name: displayName,
version: installed.version, version: installed.version,
size: undefined, size: undefined,
@ -153,13 +153,13 @@ export const VersionsTab = () => {
} }
}); });
return versions.sort((a, b) => { return backends.sort((a, b) => {
if (a.isInstalled && !b.isInstalled) return -1; if (a.isInstalled && !b.isInstalled) return -1;
if (!a.isInstalled && b.isInstalled) return 1; if (!a.isInstalled && b.isInstalled) return 1;
return 0; return 0;
}); });
}, [availableDownloads, installedVersions, currentVersion]); }, [availableDownloads, installedBackends, currentBackend]);
useEffect(() => { useEffect(() => {
if (downloading && downloadingItemRef.current) { if (downloading && downloadingItemRef.current) {
@ -170,8 +170,8 @@ export const VersionsTab = () => {
} }
}, [downloading]); }, [downloading]);
const handleDownload = async (version: VersionInfo) => { const handleDownload = async (backend: VersionInfo) => {
const download = availableDownloads.find((d) => d.name === version.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) return;
await handleDownloadFromStore({ await handleDownloadFromStore({
@ -180,59 +180,59 @@ export const VersionsTab = () => {
wasCurrentBinary: false, wasCurrentBinary: false,
}); });
await loadInstalledVersions(); await loadInstalledBackends();
}; };
const handleUpdate = async (version: VersionInfo) => { const handleUpdate = async (backend: VersionInfo) => {
const download = availableDownloads.find((d) => d.name === version.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) return;
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download, item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: version.isCurrent, wasCurrentBinary: backend.isCurrent,
oldVersionPath: version.installedPath, oldVersionPath: backend.installedPath,
}); });
await loadInstalledVersions(); await loadInstalledBackends();
}; };
const handleRedownload = async (version: VersionInfo) => { const handleRedownload = async (backend: VersionInfo) => {
const download = availableDownloads.find((d) => d.name === version.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) return;
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download, item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: version.isCurrent, wasCurrentBinary: backend.isCurrent,
oldVersionPath: version.installedPath, oldVersionPath: backend.installedPath,
}); });
await loadInstalledVersions(); await loadInstalledBackends();
}; };
const handleDelete = async (version: VersionInfo) => { const handleDelete = async (backend: VersionInfo) => {
if (!version.installedPath || version.isCurrent) return; if (!backend.installedPath || backend.isCurrent) return;
const result = await window.electronAPI.kobold.deleteRelease( const result = await window.electronAPI.kobold.deleteRelease(
version.installedPath backend.installedPath
); );
if (result.success) { if (result.success) {
await loadInstalledVersions(); await loadInstalledBackends();
} }
}; };
const makeCurrent = (version: VersionInfo) => { const makeCurrent = (backend: VersionInfo) => {
if (!version.installedPath) return; if (!backend.installedPath) return;
const targetInstalledVersion = installedVersions.find( const targetBackend = installedBackends.find(
(v) => v.path === version.installedPath (b) => b.path === backend.installedPath
); );
if (targetInstalledVersion) { if (targetBackend) {
setCurrentVersion(targetInstalledVersion); setCurrentBackend(targetBackend);
} }
window.electronAPI.kobold.setCurrentVersion(version.installedPath); window.electronAPI.kobold.setCurrentBackend(backend.installedPath);
}; };
if (loadingInstalled || loadingPlatform || loadingRemote) { if (loadingInstalled || loadingPlatform || loadingRemote) {
@ -242,9 +242,9 @@ export const VersionsTab = () => {
<Loader size="lg" /> <Loader size="lg" />
<Text c="dimmed"> <Text c="dimmed">
{loadingInstalled && (loadingPlatform || loadingRemote) {loadingInstalled && (loadingPlatform || loadingRemote)
? 'Loading versions...' ? 'Loading backends...'
: loadingInstalled : loadingInstalled
? 'Scanning installed versions...' ? 'Scanning installed backends...'
: 'Checking for updates...'} : 'Checking for updates...'}
</Text> </Text>
</Stack> </Stack>
@ -276,50 +276,50 @@ export const VersionsTab = () => {
)} )}
</Group> </Group>
{allVersions.map((version, index) => { {allBackends.map((backend, index) => {
const isDownloading = downloading === version.name; const isDownloading = downloading === backend.name;
return ( return (
<div <div
key={`${version.name}-${version.version}-${index}`} key={`${backend.name}-${backend.version}-${index}`}
style={{ paddingBottom: '0.5rem' }} style={{ paddingBottom: '0.5rem' }}
ref={isDownloading ? downloadingItemRef : null} ref={isDownloading ? downloadingItemRef : null}
> >
<DownloadCard <DownloadCard
version={version} version={backend}
size={ size={
version.size backend.size
? formatDownloadSize(version.size, version.downloadUrl) ? formatDownloadSize(backend.size, backend.downloadUrl)
: '' : ''
} }
description={getAssetDescription(version.name)} description={getAssetDescription(backend.name)}
disabled={downloading !== null} disabled={downloading !== null}
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload(version); handleDownload(backend);
}} }}
onUpdate={(e) => { onUpdate={(e) => {
e.stopPropagation(); e.stopPropagation();
handleUpdate(version); handleUpdate(backend);
}} }}
onRedownload={(e) => { onRedownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleRedownload(version); handleRedownload(backend);
}} }}
onDelete={(e) => { onDelete={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDelete(version); handleDelete(backend);
}} }}
onMakeCurrent={() => makeCurrent(version)} onMakeCurrent={() => makeCurrent(backend)}
/> />
</div> </div>
); );
})} })}
{allVersions.length === 0 && ( {allBackends.length === 0 && (
<Card withBorder radius="md" padding="md"> <Card withBorder radius="md" padding="md">
<Text size="sm" c="dimmed" ta="center"> <Text size="sm" c="dimmed" ta="center">
No versions found No backends found
</Text> </Text>
</Card> </Card>
)} )}

View file

@ -10,7 +10,7 @@ import {
Wrench, Wrench,
} from 'lucide-react'; } from 'lucide-react';
import { GeneralTab } from '@/components/settings/GeneralTab'; import { GeneralTab } from '@/components/settings/GeneralTab';
import { VersionsTab } from '@/components/settings/VersionsTab'; import { BackendsTab } from '@/components/settings/BackendsTab';
import { AppearanceTab } from '@/components/settings/AppearanceTab'; import { AppearanceTab } from '@/components/settings/AppearanceTab';
import { SystemTab } from '@/components/settings/SystemTab'; import { SystemTab } from '@/components/settings/SystemTab';
import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab'; import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab';
@ -33,11 +33,11 @@ export const SettingsModal = ({
}: SettingsModalProps) => { }: SettingsModalProps) => {
const [activeTab, setActiveTab] = useState('general'); const [activeTab, setActiveTab] = useState('general');
const showVersionsTab = const showBackendsTab =
currentScreen !== 'download' && currentScreen !== 'welcome'; currentScreen !== 'download' && currentScreen !== 'welcome';
const effectiveActiveTab = const effectiveActiveTab =
!showVersionsTab && activeTab === 'versions' ? 'general' : activeTab; !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
@ -110,14 +110,14 @@ export const SettingsModal = ({
> >
General General
</Tabs.Tab> </Tabs.Tab>
{showVersionsTab && ( {showBackendsTab && (
<Tabs.Tab <Tabs.Tab
value="versions" value="backends"
leftSection={ leftSection={
<GitBranch style={{ width: rem(16), height: rem(16) }} /> <GitBranch style={{ width: rem(16), height: rem(16) }} />
} }
> >
Versions Backends
</Tabs.Tab> </Tabs.Tab>
)} )}
<Tabs.Tab <Tabs.Tab
@ -156,9 +156,9 @@ export const SettingsModal = ({
<GeneralTab /> <GeneralTab />
</Tabs.Panel> </Tabs.Panel>
{showVersionsTab && ( {showBackendsTab && (
<Tabs.Panel value="versions"> <Tabs.Panel value="backends">
<VersionsTab /> <BackendsTab />
</Tabs.Panel> </Tabs.Panel>
)} )}

View file

@ -26,7 +26,7 @@ export const SystemTab = () => {
const loadKoboldVersion = async () => { const loadKoboldVersion = async () => {
const currentVersion = const currentVersion =
await window.electronAPI.kobold.getCurrentVersion(); await window.electronAPI.kobold.getCurrentBackend();
if (currentVersion) { if (currentVersion) {
setKoboldVersion(currentVersion.version); setKoboldVersion(currentVersion.version);
} }

View file

@ -206,11 +206,11 @@ const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs) => {
} }
}; };
const buildBackendArgs = (launchArgs: LaunchArgs) => { const buildBackendArgs = (launchArgs: LaunchArgs, platform: string) => {
const args: string[] = []; const args: string[] = [];
if (!launchArgs.backend || launchArgs.backend === 'cpu') { if (!launchArgs.backend || launchArgs.backend === 'cpu') {
if (launchArgs.backend === 'cpu') { if (launchArgs.backend === 'cpu' && platform !== 'darwin') {
args.push('--usecpu'); args.push('--usecpu');
} }
return args; return args;
@ -271,10 +271,12 @@ export const useLaunchLogic = ({
onLaunch(); onLaunch();
const platform = await window.electronAPI.kobold.getPlatform();
const args: string[] = [ const args: string[] = [
...buildModelArgs(model, sdmodel, launchArgs), ...buildModelArgs(model, sdmodel, launchArgs),
...buildConfigArgs(hasImageModel, launchArgs), ...buildConfigArgs(hasImageModel, launchArgs),
...buildBackendArgs(launchArgs), ...buildBackendArgs(launchArgs, platform),
]; ];
if (launchArgs.additionalArguments.trim()) { if (launchArgs.additionalArguments.trim()) {

View file

@ -6,11 +6,11 @@ import {
} from '@/utils/version'; } from '@/utils/version';
import { useKoboldVersionsStore } from '@/stores/koboldVersions'; import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { getROCmDownload } from '@/utils/rocm'; import { getROCmDownload } from '@/utils/rocm';
import type { InstalledVersion, DownloadItem } from '@/types/electron'; import type { InstalledBackend, DownloadItem } from '@/types/electron';
import type { DismissedUpdate } from '@/types'; import type { DismissedUpdate } from '@/types';
export interface BinaryUpdateInfo { export interface BinaryUpdateInfo {
currentVersion: InstalledVersion; currentBackend: InstalledBackend;
availableUpdate: DownloadItem; availableUpdate: DownloadItem;
} }
@ -44,12 +44,12 @@ export const useUpdateChecker = () => {
} }
setIsChecking(true); setIsChecking(true);
const [currentVersion, rocmDownload] = await Promise.all([ const [currentBackend, rocmDownload] = await Promise.all([
window.electronAPI.kobold.getCurrentVersion(), window.electronAPI.kobold.getCurrentBackend(),
getROCmDownload(), getROCmDownload(),
]); ]);
if (!currentVersion) { if (!currentBackend) {
setIsChecking(false); setIsChecking(false);
return; return;
} }
@ -59,7 +59,7 @@ export const useUpdateChecker = () => {
availableDownloads.push(rocmDownload); availableDownloads.push(rocmDownload);
} }
const currentDisplayName = getDisplayNameFromPath(currentVersion); const currentDisplayName = getDisplayNameFromPath(currentBackend);
const matchingDownload = availableDownloads.find( const matchingDownload = availableDownloads.find(
(download: DownloadItem) => { (download: DownloadItem) => {
@ -70,18 +70,18 @@ export const useUpdateChecker = () => {
if (matchingDownload && matchingDownload.version) { if (matchingDownload && matchingDownload.version) {
const hasUpdate = const hasUpdate =
compareVersions(matchingDownload.version, currentVersion.version) > 0; compareVersions(matchingDownload.version, currentBackend.version) > 0;
if (hasUpdate) { if (hasUpdate) {
const isUpdateDismissed = dismissedUpdates.some( const isUpdateDismissed = dismissedUpdates.some(
(dismissedUpdate) => (dismissedUpdate) =>
dismissedUpdate.currentVersionPath === currentVersion.path && dismissedUpdate.currentVersionPath === currentBackend.path &&
dismissedUpdate.targetVersion === matchingDownload.version dismissedUpdate.targetVersion === matchingDownload.version
); );
if (!isUpdateDismissed) { if (!isUpdateDismissed) {
setUpdateInfo({ setUpdateInfo({
currentVersion, currentBackend,
availableUpdate: matchingDownload, availableUpdate: matchingDownload,
}); });
setShowUpdateModal(true); setShowUpdateModal(true);
@ -95,7 +95,7 @@ export const useUpdateChecker = () => {
const skipUpdate = useCallback(() => { const skipUpdate = useCallback(() => {
if (updateInfo && updateInfo.availableUpdate.version) { if (updateInfo && updateInfo.availableUpdate.version) {
const newDismissedUpdate: DismissedUpdate = { const newDismissedUpdate: DismissedUpdate = {
currentVersionPath: updateInfo.currentVersion.path, currentVersionPath: updateInfo.currentBackend.path,
targetVersion: updateInfo.availableUpdate.version, targetVersion: updateInfo.availableUpdate.version,
}; };

View file

@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import { CPUCapabilities, GPUDevice } from '@/types/hardware'; import { CPUCapabilities, GPUDevice } from '@/types/hardware';
import type { BackendOption, BackendSupport } from '@/types'; import type { AccelerationOption, AccelerationSupport } from '@/types';
export interface Warning { export interface Warning {
type: 'warning' | 'info'; type: 'warning' | 'info';
@ -58,14 +58,14 @@ interface GpuInfo {
} }
const checkGpuWarnings = async ( const checkGpuWarnings = async (
backendSupport: BackendSupport, accelerationSupport: AccelerationSupport,
gpuCapabilities: GpuCapabilities, gpuCapabilities: GpuCapabilities,
gpuInfo: GpuInfo gpuInfo: GpuInfo
) => { ) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
if ( if (
backendSupport.cuda && accelerationSupport.cuda &&
gpuCapabilities.cuda.devices.length === 0 && gpuCapabilities.cuda.devices.length === 0 &&
gpuInfo.hasNVIDIA gpuInfo.hasNVIDIA
) { ) {
@ -77,7 +77,7 @@ const checkGpuWarnings = async (
} }
if ( if (
backendSupport.rocm && accelerationSupport.rocm &&
gpuCapabilities.rocm.devices.length === 0 && gpuCapabilities.rocm.devices.length === 0 &&
gpuInfo.hasAMD gpuInfo.hasAMD
) { ) {
@ -134,7 +134,7 @@ const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
const checkCpuWarnings = ( const checkCpuWarnings = (
backend: string, backend: string,
availableBackends: BackendOption[] availableAccelerations: AccelerationOption[]
) => { ) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
@ -143,8 +143,8 @@ const checkCpuWarnings = (
} }
if ( if (
availableBackends.length > 0 && availableAccelerations.length > 0 &&
availableBackends.some((b) => b.value === 'cpu') availableAccelerations.some((a) => a.value === 'cpu')
) { ) {
warnings.push({ warnings.push({
type: 'info', type: 'info',
@ -159,35 +159,35 @@ const checkCpuWarnings = (
const checkBackendWarnings = async (params?: { const checkBackendWarnings = async (params?: {
backend: string; backend: string;
cpuCapabilities: CPUCapabilities | null; cpuCapabilities: CPUCapabilities | null;
availableBackends: BackendOption[]; availableAccelerations: AccelerationOption[];
}) => { }) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
const [backendSupport, gpuCapabilities, gpuInfo] = await Promise.all([ const [accelerationSupport, gpuCapabilities, gpuInfo] = await Promise.all([
window.electronAPI.kobold.detectBackendSupport(), window.electronAPI.kobold.detectAccelerationSupport(),
window.electronAPI.kobold.detectGPUCapabilities(), window.electronAPI.kobold.detectGPUCapabilities(),
window.electronAPI.kobold.detectGPU(), window.electronAPI.kobold.detectGPU(),
]); ]);
if (!backendSupport) { if (!accelerationSupport) {
return warnings; return warnings;
} }
const gpuWarnings = await checkGpuWarnings( const gpuWarnings = await checkGpuWarnings(
backendSupport, accelerationSupport,
gpuCapabilities, gpuCapabilities,
gpuInfo gpuInfo
); );
warnings.push(...gpuWarnings); warnings.push(...gpuWarnings);
if (params) { if (params) {
const { backend, cpuCapabilities, availableBackends } = params; const { backend, cpuCapabilities, availableAccelerations } = params;
const vramWarnings = await checkVramWarnings(backend); const vramWarnings = await checkVramWarnings(backend);
warnings.push(...vramWarnings); warnings.push(...vramWarnings);
if (cpuCapabilities) { if (cpuCapabilities) {
const cpuWarnings = checkCpuWarnings(backend, availableBackends); const cpuWarnings = checkCpuWarnings(backend, availableAccelerations);
warnings.push(...cpuWarnings); warnings.push(...cpuWarnings);
} }
} }
@ -214,15 +214,15 @@ export const useWarnings = ({
return; return;
} }
const [cpuCapabilitiesResult, availableBackends] = await Promise.all([ const [cpuCapabilitiesResult, availableAccelerations] = await Promise.all([
window.electronAPI.kobold.detectCPU(), window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.getAvailableBackends(), window.electronAPI.kobold.getAvailableAccelerations(),
]); ]);
const result = await checkBackendWarnings({ const result = await checkBackendWarnings({
backend, backend,
cpuCapabilities: cpuCapabilitiesResult, cpuCapabilities: cpuCapabilitiesResult,
availableBackends, availableAccelerations,
}); });
setBackendWarnings(result); setBackendWarnings(result);

View file

@ -8,11 +8,11 @@ import {
} from '@/main/modules/koboldcpp/launcher'; } from '@/main/modules/koboldcpp/launcher';
import { downloadRelease } from '@/main/modules/koboldcpp/download'; import { downloadRelease } from '@/main/modules/koboldcpp/download';
import { import {
getInstalledVersions, getInstalledBackends,
getCurrentVersion, getCurrentBackend,
setCurrentVersion, setCurrentBackend,
deleteRelease, deleteRelease,
} from '@/main/modules/koboldcpp/version'; } from '@/main/modules/koboldcpp/backend';
import { import {
getConfigFiles, getConfigFiles,
saveConfigFile, saveConfigFile,
@ -61,9 +61,9 @@ import {
detectSystemMemory, detectSystemMemory,
} from '@/main/modules/hardware'; } from '@/main/modules/hardware';
import { import {
detectBackendSupport, detectAccelerationSupport,
getAvailableBackends, getAvailableAccelerations,
} from '@/main/modules/koboldcpp/backend'; } from '@/main/modules/koboldcpp/acceleration';
import { import {
openPerformanceManager, openPerformanceManager,
startMonitoring, startMonitoring,
@ -85,9 +85,9 @@ export function setupIPCHandlers() {
downloadRelease(asset, options) downloadRelease(asset, options)
); );
ipcMain.handle('kobold:getInstalledVersions', () => getInstalledVersions()); ipcMain.handle('kobold:getInstalledBackends', () => getInstalledBackends());
ipcMain.handle('kobold:getCurrentVersion', () => getCurrentVersion()); ipcMain.handle('kobold:getCurrentBackend', () => getCurrentBackend());
ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles()); ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles());
@ -105,8 +105,8 @@ export function setupIPCHandlers() {
setConfig('selectedConfig', configName) setConfig('selectedConfig', configName)
); );
ipcMain.handle('kobold:setCurrentVersion', (_, version) => ipcMain.handle('kobold:setCurrentBackend', (_, version) =>
setCurrentVersion(version) setCurrentBackend(version)
); );
ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir()); ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir());
@ -127,10 +127,13 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:detectROCm', () => detectROCm()); ipcMain.handle('kobold:detectROCm', () => detectROCm());
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport()); ipcMain.handle('kobold:detectAccelerationSupport', () =>
detectAccelerationSupport()
);
ipcMain.handle('kobold:getAvailableBackends', (_, includeDisabled = false) => ipcMain.handle(
getAvailableBackends(includeDisabled) 'kobold:getAvailableAccelerations',
(_, includeDisabled = false) => getAvailableAccelerations(includeDisabled)
); );
ipcMain.handle('kobold:getPlatform', () => platform); ipcMain.handle('kobold:getPlatform', () => platform);

View file

@ -0,0 +1,184 @@
import { join, dirname } from 'path';
import { platform } from 'process';
import { pathExists } from '@/utils/node/fs';
import { getCurrentBinaryInfo } from './backend';
import { detectGPUCapabilities, detectCPU } from '../hardware';
import { tryExecute, safeExecute } from '@/utils/node/logging';
import type { AccelerationOption, AccelerationSupport } from '@/types';
const accelerationSupportCache = new Map<string, AccelerationSupport>();
const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
const CPU_LABEL = platform === 'darwin' ? 'Metal' : 'CPU';
async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
if (accelerationSupportCache.has(koboldBinaryPath)) {
return accelerationSupportCache.get(koboldBinaryPath)!;
}
const support: AccelerationSupport = {
rocm: false,
vulkan: false,
clblast: false,
noavx2: false,
failsafe: false,
cuda: false,
};
await tryExecute(async () => {
const binaryDir = dirname(koboldBinaryPath);
const internalDir = join(binaryDir, '_internal');
const libExtension = platform === 'win32' ? '.dll' : '.so';
const hasKoboldCppLib = async (name: string): Promise<boolean> => {
const filename = `${name}${libExtension}`;
if (platform === 'win32') {
return (
(await pathExists(join(binaryDir, filename))) ||
(await pathExists(join(internalDir, filename)))
);
} else {
return (
(await pathExists(join(internalDir, filename))) ||
(await pathExists(join(binaryDir, filename)))
);
}
};
const [rocm, vulkan, clblast, noavx2, failsafe, cuda] = await Promise.all([
hasKoboldCppLib('koboldcpp_hipblas'),
hasKoboldCppLib('koboldcpp_vulkan'),
hasKoboldCppLib('koboldcpp_clblast'),
hasKoboldCppLib('koboldcpp_noavx2'),
hasKoboldCppLib('koboldcpp_failsafe'),
hasKoboldCppLib('koboldcpp_cublas'),
]);
support.rocm = rocm;
support.vulkan = vulkan;
support.clblast = clblast;
support.noavx2 = noavx2;
support.failsafe = failsafe;
support.cuda = cuda;
}, 'Error detecting acceleration support');
accelerationSupportCache.set(koboldBinaryPath, support);
return support;
}
export const detectAccelerationSupport = async () =>
(await safeExecute(async () => {
const currentBinaryInfo = await getCurrentBinaryInfo();
if (!currentBinaryInfo?.path) {
return null;
}
return detectAccelerationSupportFromPath(currentBinaryInfo.path);
}, 'Error detecting current binary acceleration support')) || null;
export async function getAvailableAccelerations(includeDisabled = false) {
if (platform === 'darwin') {
return [{ value: 'cpu', label: CPU_LABEL }];
}
// eslint-disable-next-line sonarjs/cognitive-complexity
const result = await safeExecute(async () => {
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] =
await Promise.all([
getCurrentBinaryInfo(),
detectGPUCapabilities(),
includeDisabled ? detectCPU() : Promise.resolve(null),
]);
if (!currentBinaryInfo?.path) {
return [{ value: 'cpu', label: CPU_LABEL }];
}
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
if (availableAccelerationsCache.has(cacheKey)) {
return availableAccelerationsCache.get(cacheKey)!;
}
const accelerationSupport = await detectAccelerationSupport();
if (!accelerationSupport) {
return [];
}
const accelerations: AccelerationOption[] = [];
if (accelerationSupport.cuda) {
const isSupported = hardwareCapabilities.cuda.devices.length > 0;
if (isSupported || includeDisabled) {
accelerations.push({
value: 'cuda',
label: 'CUDA',
devices: hardwareCapabilities.cuda.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (accelerationSupport.rocm) {
const isSupported = hardwareCapabilities.rocm.devices.length > 0;
if (isSupported || includeDisabled) {
accelerations.push({
value: 'rocm',
label: 'ROCm',
devices: hardwareCapabilities.rocm.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (accelerationSupport.vulkan) {
const isSupported = hardwareCapabilities.vulkan.devices.length > 0;
if (isSupported || includeDisabled) {
accelerations.push({
value: 'vulkan',
label: 'Vulkan',
devices: hardwareCapabilities.vulkan.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (accelerationSupport.clblast) {
const discreteDevices = hardwareCapabilities.clblast.devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated
);
const isSupported = discreteDevices.length > 0;
if (isSupported || includeDisabled) {
accelerations.push({
value: 'clblast',
label: 'CLBlast',
devices: hardwareCapabilities.clblast.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
accelerations.push({
value: 'cpu',
label: CPU_LABEL,
devices: cpuCapabilities?.devices.map((device) => device.name) || [],
disabled: false,
});
if (includeDisabled) {
accelerations.sort((a, b) => {
if (a.disabled === b.disabled) return 0;
return a.disabled ? 1 : -1;
});
}
availableAccelerationsCache.set(cacheKey, accelerations);
return accelerations;
}, 'Failed to get available accelerations');
return result || [{ value: 'cpu', label: CPU_LABEL }];
}

View file

@ -1,178 +1,233 @@
import { join, dirname } from 'path'; import { readdir, stat, rm } from 'fs/promises';
import { platform } from 'process'; import { join } from 'path';
import { execa } from 'execa';
import {
getCurrentKoboldBinary,
setCurrentKoboldBinary,
getInstallDir,
} from '../config';
import { sendToRenderer } from '../window';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { getCurrentBinaryInfo } from './version'; import { logError } from '@/utils/node/logging';
import { detectGPUCapabilities, detectCPU } from '../hardware'; import { getLauncherPath } from '@/utils/node/path';
import { tryExecute, safeExecute } from '@/utils/node/logging'; import type { InstalledBackend } from '@/types/electron';
import type { BackendOption, BackendSupport } from '@/types';
const backendSupportCache = new Map<string, BackendSupport>(); const versionCache = new Map<
const availableBackendsCache = new Map<string, BackendOption[]>(); string,
{ version: string; actualVersion?: string } | null
>();
async function detectBackendSupportFromPath(koboldBinaryPath: string) { export function clearVersionCache(path?: string) {
if (backendSupportCache.has(koboldBinaryPath)) { if (path) {
return backendSupportCache.get(koboldBinaryPath)!; versionCache.delete(path);
} else {
versionCache.clear();
} }
const support: BackendSupport = {
rocm: false,
vulkan: false,
clblast: false,
noavx2: false,
failsafe: false,
cuda: false,
};
await tryExecute(async () => {
const binaryDir = dirname(koboldBinaryPath);
const internalDir = join(binaryDir, '_internal');
const libExtension = platform === 'win32' ? '.dll' : '.so';
const hasKoboldCppLib = async (name: string): Promise<boolean> => {
const filename = `${name}${libExtension}`;
if (platform === 'win32') {
return (
(await pathExists(join(binaryDir, filename))) ||
(await pathExists(join(internalDir, filename)))
);
} else {
return (
(await pathExists(join(internalDir, filename))) ||
(await pathExists(join(binaryDir, filename)))
);
}
};
const [rocm, vulkan, clblast, noavx2, failsafe, cuda] = await Promise.all([
hasKoboldCppLib('koboldcpp_hipblas'),
hasKoboldCppLib('koboldcpp_vulkan'),
hasKoboldCppLib('koboldcpp_clblast'),
hasKoboldCppLib('koboldcpp_noavx2'),
hasKoboldCppLib('koboldcpp_failsafe'),
hasKoboldCppLib('koboldcpp_cublas'),
]);
support.rocm = rocm;
support.vulkan = vulkan;
support.clblast = clblast;
support.noavx2 = noavx2;
support.failsafe = failsafe;
support.cuda = cuda;
}, 'Error detecting backend support');
backendSupportCache.set(koboldBinaryPath, support);
return support;
} }
export const detectBackendSupport = async () => export async function getInstalledBackends() {
(await safeExecute(async () => { try {
const currentBinaryInfo = await getCurrentBinaryInfo(); const installDir = getInstallDir();
if (!(await pathExists(installDir))) {
if (!currentBinaryInfo?.path) {
return null;
}
return detectBackendSupportFromPath(currentBinaryInfo.path);
}, 'Error detecting current binary backend support')) || null;
export async function getAvailableBackends(includeDisabled = false) {
// eslint-disable-next-line sonarjs/cognitive-complexity
const result = await safeExecute(async () => {
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] =
await Promise.all([
getCurrentBinaryInfo(),
detectGPUCapabilities(),
includeDisabled ? detectCPU() : Promise.resolve(null),
]);
if (!currentBinaryInfo?.path) {
return [{ value: 'cpu', label: 'CPU' }];
}
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
if (availableBackendsCache.has(cacheKey)) {
return availableBackendsCache.get(cacheKey)!;
}
const backendSupport = await detectBackendSupport();
if (!backendSupport) {
return []; return [];
} }
const backends: BackendOption[] = []; const items = await readdir(installDir);
const launchers: { path: string; filename: string; size: number }[] = [];
if (backendSupport.cuda) { for (const item of items) {
const isSupported = hardwareCapabilities.cuda.devices.length > 0; const itemPath = join(installDir, item);
if (isSupported || includeDisabled) { const stats = await stat(itemPath);
backends.push({
value: 'cuda', if (stats.isDirectory()) {
label: 'CUDA', const launcherPath = await getLauncherPath(itemPath);
devices: hardwareCapabilities.cuda.devices, if (launcherPath && (await pathExists(launcherPath))) {
disabled: includeDisabled ? !isSupported : undefined, const launcherStats = await stat(launcherPath);
}); const launcherFilename = launcherPath.split(/[/\\]/).pop() || '';
launchers.push({
path: launcherPath,
filename: launcherFilename,
size: launcherStats.size,
});
}
} }
} }
if (backendSupport.rocm) { const versionPromises = launchers.map(async (launcher) => {
const isSupported = hardwareCapabilities.rocm.devices.length > 0; try {
if (isSupported || includeDisabled) { const versionInfo = await getVersionFromBinary(launcher.path);
backends.push({
value: 'rocm',
label: 'ROCm',
devices: hardwareCapabilities.rocm.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (backendSupport.vulkan) { if (!versionInfo) {
const isSupported = hardwareCapabilities.vulkan.devices.length > 0; return null;
if (isSupported || includeDisabled) { }
backends.push({
value: 'vulkan',
label: 'Vulkan',
devices: hardwareCapabilities.vulkan.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (backendSupport.clblast) { return {
const discreteDevices = hardwareCapabilities.clblast.devices.filter( version: versionInfo.version,
(device) => typeof device === 'string' || !device.isIntegrated path: launcher.path,
); filename: launcher.filename,
const isSupported = discreteDevices.length > 0; size: launcher.size,
if (isSupported || includeDisabled) { actualVersion: versionInfo.actualVersion,
backends.push({ } as InstalledBackend;
value: 'clblast', } catch (error) {
label: 'CLBlast', logError(
devices: hardwareCapabilities.clblast.devices, `Could not detect version for ${launcher.filename}:`,
disabled: includeDisabled ? !isSupported : undefined, error as Error
}); );
return null;
} }
}
backends.push({
value: 'cpu',
label: 'CPU',
devices: cpuCapabilities?.devices.map((device) => device.name) || [],
disabled: false,
}); });
if (includeDisabled) { const results = await Promise.all(versionPromises);
backends.sort((a, b) => { return results.filter(
if (a.disabled === b.disabled) return 0; (version): version is InstalledBackend => version !== null
return a.disabled ? 1 : -1; );
}); } catch (error) {
logError('Error scanning install directory:', error as Error);
return [];
}
}
export async function getCurrentBackend() {
const currentBinaryPath = getCurrentKoboldBinary();
const backends = await getInstalledBackends();
if (currentBinaryPath && (await pathExists(currentBinaryPath))) {
const currentBackend = backends.find(
(b: InstalledBackend) => b.path === currentBinaryPath
);
if (currentBackend) {
return currentBackend;
}
}
const firstBackend = backends[0];
if (firstBackend) {
await setCurrentKoboldBinary(firstBackend.path);
return firstBackend;
}
if (currentBinaryPath) {
await setCurrentKoboldBinary('');
}
return null;
}
export async function getCurrentBinaryInfo() {
const currentBackend = await getCurrentBackend();
if (currentBackend) {
const pathParts = currentBackend.path.split(/[/\\]/);
const filename = pathParts[pathParts.length - 2] || currentBackend.filename;
return {
path: currentBackend.path,
filename,
};
}
return null;
}
export async function setCurrentBackend(binaryPath: string) {
if (await pathExists(binaryPath)) {
await setCurrentKoboldBinary(binaryPath);
sendToRenderer('versions-updated');
return true;
}
return false;
}
export async function deleteRelease(binaryPath: string) {
try {
if (!(await pathExists(binaryPath))) {
return { success: false, error: 'Release not found' };
} }
availableBackendsCache.set(cacheKey, backends); const currentBinaryPath = getCurrentKoboldBinary();
return backends; if (currentBinaryPath === binaryPath) {
}, 'Failed to get available backends'); return {
success: false,
error: 'Cannot delete the currently active release',
};
}
return result || [{ value: 'cpu', label: 'CPU' }]; const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
if (await pathExists(releaseDir)) {
await rm(releaseDir, { recursive: true, force: true });
clearVersionCache(binaryPath);
sendToRenderer('versions-updated');
return { success: true };
}
return { success: false, error: 'Release directory not found' };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
export async function getVersionFromBinary(launcherPath: string) {
try {
if (!(await pathExists(launcherPath))) {
return null;
}
if (versionCache.has(launcherPath)) {
return versionCache.get(launcherPath);
}
let folderVersion: string | null = null;
let actualVersion: string | null = null;
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
if (folderName) {
const versionMatch = folderName.match(
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
);
if (versionMatch) {
folderVersion = versionMatch[1];
}
}
try {
const result = await execa(launcherPath, ['--version'], {
timeout: 30000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const allOutput = (result.stdout + result.stderr).trim();
const lines = allOutput.split('\n').filter((line) => line.trim());
if (lines.length > 0) {
const lastLine = lines[lines.length - 1].trim();
const versionMatch = lastLine.match(
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
);
if (versionMatch) {
actualVersion = versionMatch[1];
}
}
} catch {}
const result = {
version: folderVersion || actualVersion || 'unknown',
actualVersion:
folderVersion && actualVersion && folderVersion !== actualVersion
? actualVersion
: undefined,
};
versionCache.set(launcherPath, result);
return result;
} catch {
versionCache.set(launcherPath, null);
return null;
}
} }

View file

@ -15,7 +15,7 @@ import { pathExists } from '@/utils/node/fs';
import { stripAssetExtensions } from '@/utils/version'; import { stripAssetExtensions } from '@/utils/version';
import { getLauncherPath } from '@/utils/node/path'; import { getLauncherPath } from '@/utils/node/path';
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron'; import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
import { clearVersionCache } from './version'; import { clearVersionCache } from './backend';
async function removeDirectoryWithRetry( async function removeDirectoryWithRetry(
dirPath: string, dirPath: string,

View file

@ -7,7 +7,7 @@ import { sendKoboldOutput } from '@/main/modules/window';
import { SERVER_READY_SIGNALS } from '@/constants'; import { SERVER_READY_SIGNALS } from '@/constants';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { getCurrentVersion } from '../version'; import { getCurrentBackend } from '../backend';
import { import {
getCurrentKoboldBinary, getCurrentKoboldBinary,
get as getConfig, get as getConfig,
@ -153,15 +153,15 @@ export async function launchKoboldCpp(
spawnPreLaunchCommands(preLaunchCommands); spawnPreLaunchCommands(preLaunchCommands);
} }
const currentVersion = await getCurrentVersion(); const currentBackend = await getCurrentBackend();
if (!currentVersion || !(await pathExists(currentVersion.path))) { if (!currentBackend || !(await pathExists(currentBackend.path))) {
const rawPath = getCurrentKoboldBinary(); const rawPath = getCurrentKoboldBinary();
const error = currentVersion const error = currentBackend
? `Binary file does not exist at path: ${currentVersion.path}` ? `Binary file does not exist at path: ${currentBackend.path}`
: 'No version configured'; : 'No backend configured';
logError( logError(
`Launch failed: ${error}. Raw config path: "${rawPath}", Current version: ${JSON.stringify(currentVersion)}` `Launch failed: ${error}. Raw config path: "${rawPath}", Current backend: ${JSON.stringify(currentBackend)}`
); );
return { return {
@ -170,7 +170,7 @@ export async function launchKoboldCpp(
}; };
} }
const binaryDir = currentVersion.path.split(/[/\\]/).slice(0, -1).join('/'); const binaryDir = currentBackend.path.split(/[/\\]/).slice(0, -1).join('/');
const { isImageMode, isTextMode, debugmode } = parseKoboldConfig(args); const { isImageMode, isTextMode, debugmode } = parseKoboldConfig(args);
@ -191,14 +191,14 @@ export async function launchKoboldCpp(
await startProxy(koboldHost, koboldPort); await startProxy(koboldHost, koboldPort);
const child = spawn(currentVersion.path, finalArgs, { const child = spawn(currentBackend.path, finalArgs, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
}); });
koboldProcess = child; koboldProcess = child;
const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`; const commandLine = `${currentBackend.path} ${finalArgs.join(' ')}`;
sendKoboldOutput(commandLine); sendKoboldOutput(commandLine);

View file

@ -1,233 +0,0 @@
import { readdir, stat, rm } from 'fs/promises';
import { join } from 'path';
import { execa } from 'execa';
import {
getCurrentKoboldBinary,
setCurrentKoboldBinary,
getInstallDir,
} from '../config';
import { sendToRenderer } from '../window';
import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path';
import type { InstalledVersion } from '@/types/electron';
const versionCache = new Map<
string,
{ version: string; actualVersion?: string } | null
>();
export function clearVersionCache(path?: string) {
if (path) {
versionCache.delete(path);
} else {
versionCache.clear();
}
}
export async function getInstalledVersions() {
try {
const installDir = getInstallDir();
if (!(await pathExists(installDir))) {
return [];
}
const items = await readdir(installDir);
const launchers: { path: string; filename: string; size: number }[] = [];
for (const item of items) {
const itemPath = join(installDir, item);
const stats = await stat(itemPath);
if (stats.isDirectory()) {
const launcherPath = await getLauncherPath(itemPath);
if (launcherPath && (await pathExists(launcherPath))) {
const launcherStats = await stat(launcherPath);
const launcherFilename = launcherPath.split(/[/\\]/).pop() || '';
launchers.push({
path: launcherPath,
filename: launcherFilename,
size: launcherStats.size,
});
}
}
}
const versionPromises = launchers.map(async (launcher) => {
try {
const versionInfo = await getVersionFromBinary(launcher.path);
if (!versionInfo) {
return null;
}
return {
version: versionInfo.version,
path: launcher.path,
filename: launcher.filename,
size: launcher.size,
actualVersion: versionInfo.actualVersion,
} as InstalledVersion;
} catch (error) {
logError(
`Could not detect version for ${launcher.filename}:`,
error as Error
);
return null;
}
});
const results = await Promise.all(versionPromises);
return results.filter(
(version): version is InstalledVersion => version !== null
);
} catch (error) {
logError('Error scanning install directory:', error as Error);
return [];
}
}
export async function getCurrentVersion() {
const currentBinaryPath = getCurrentKoboldBinary();
const versions = await getInstalledVersions();
if (currentBinaryPath && (await pathExists(currentBinaryPath))) {
const currentVersion = versions.find(
(v: InstalledVersion) => v.path === currentBinaryPath
);
if (currentVersion) {
return currentVersion;
}
}
const firstVersion = versions[0];
if (firstVersion) {
await setCurrentKoboldBinary(firstVersion.path);
return firstVersion;
}
if (currentBinaryPath) {
await setCurrentKoboldBinary('');
}
return null;
}
export async function getCurrentBinaryInfo() {
const currentVersion = await getCurrentVersion();
if (currentVersion) {
const pathParts = currentVersion.path.split(/[/\\]/);
const filename = pathParts[pathParts.length - 2] || currentVersion.filename;
return {
path: currentVersion.path,
filename,
};
}
return null;
}
export async function setCurrentVersion(binaryPath: string) {
if (await pathExists(binaryPath)) {
await setCurrentKoboldBinary(binaryPath);
sendToRenderer('versions-updated');
return true;
}
return false;
}
export async function deleteRelease(binaryPath: string) {
try {
if (!(await pathExists(binaryPath))) {
return { success: false, error: 'Release not found' };
}
const currentBinaryPath = getCurrentKoboldBinary();
if (currentBinaryPath === binaryPath) {
return {
success: false,
error: 'Cannot delete the currently active release',
};
}
const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
if (await pathExists(releaseDir)) {
await rm(releaseDir, { recursive: true, force: true });
clearVersionCache(binaryPath);
sendToRenderer('versions-updated');
return { success: true };
}
return { success: false, error: 'Release directory not found' };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
export async function getVersionFromBinary(launcherPath: string) {
try {
if (!(await pathExists(launcherPath))) {
return null;
}
if (versionCache.has(launcherPath)) {
return versionCache.get(launcherPath);
}
let folderVersion: string | null = null;
let actualVersion: string | null = null;
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
if (folderName) {
const versionMatch = folderName.match(
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
);
if (versionMatch) {
folderVersion = versionMatch[1];
}
}
try {
const result = await execa(launcherPath, ['--version'], {
timeout: 30000,
stdio: ['ignore', 'pipe', 'pipe'],
});
const allOutput = (result.stdout + result.stderr).trim();
const lines = allOutput.split('\n').filter((line) => line.trim());
if (lines.length > 0) {
const lastLine = lines[lines.length - 1].trim();
const versionMatch = lastLine.match(
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
);
if (versionMatch) {
actualVersion = versionMatch[1];
}
}
} catch {}
const result = {
version: folderVersion || actualVersion || 'unknown',
actualVersion:
folderVersion && actualVersion && folderVersion !== actualVersion
? actualVersion
: undefined,
};
versionCache.set(launcherPath, result);
return result;
} catch {
versionCache.set(launcherPath, null);
return null;
}
}

View file

@ -16,10 +16,10 @@ import type {
} from '@/main/modules/monitoring'; } from '@/main/modules/monitoring';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'), getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'), getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
setCurrentVersion: (version) => setCurrentBackend: (version) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version), ipcRenderer.invoke('kobold:setCurrentBackend', version),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'), getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'), detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
@ -28,9 +28,10 @@ const koboldAPI: KoboldAPI = {
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'), detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'), detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'), detectAccelerationSupport: () =>
getAvailableBackends: (includeDisabled) => ipcRenderer.invoke('kobold:detectAccelerationSupport'),
ipcRenderer.invoke('kobold:getAvailableBackends', includeDisabled), getAvailableAccelerations: (includeDisabled) =>
ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'), ipcRenderer.invoke('kobold:selectInstallDirectory'),

View file

@ -8,7 +8,7 @@ import type {
GitHubRelease, GitHubRelease,
ReleaseWithStatus, ReleaseWithStatus,
GitHubAsset, GitHubAsset,
InstalledVersion, InstalledBackend,
} from '@/types/electron'; } from '@/types/electron';
import { sortDownloadsByType } from '@/utils/assets'; import { sortDownloadsByType } from '@/utils/assets';
@ -137,7 +137,7 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
safeExecute(async () => { safeExecute(async () => {
const [response, installedVersions] = await Promise.all([ const [response, installedVersions] = await Promise.all([
fetch(GITHUB_API.LATEST_RELEASE_URL), fetch(GITHUB_API.LATEST_RELEASE_URL),
window.electronAPI.kobold.getInstalledVersions(), window.electronAPI.kobold.getInstalledBackends(),
]); ]);
if (!response.ok) return null; if (!response.ok) return null;
@ -147,11 +147,11 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
const availableAssets = latestRelease.assets.map( const availableAssets = latestRelease.assets.map(
(asset: GitHubAsset) => { (asset: GitHubAsset) => {
const installedVersion = installedVersions.find( const installedBackend = installedVersions.find(
(v: InstalledVersion) => { (v: InstalledBackend) => {
const pathParts = v.path.split(/[/\\]/); const pathParts = v.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex( const launcherIndex = pathParts.findIndex(
(part) => (part: string) =>
part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher' ||
part === 'koboldcpp-launcher.exe' part === 'koboldcpp-launcher.exe'
); );
@ -167,8 +167,8 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
return { return {
asset, asset,
isDownloaded: !!installedVersion, isDownloaded: !!installedBackend,
installedVersion: installedVersion?.version, installedVersion: installedBackend?.version,
}; };
} }
); );

View file

@ -6,8 +6,8 @@ import type {
SystemMemoryInfo, SystemMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
import type { import type {
BackendOption, AccelerationOption,
BackendSupport, AccelerationSupport,
Screen, Screen,
ModelAnalysis, ModelAnalysis,
CachedModel, CachedModel,
@ -57,7 +57,7 @@ export interface ReleaseWithStatus {
}[]; }[];
} }
export interface InstalledVersion { export interface InstalledBackend {
version: string; version: string;
path: string; path: string;
filename: string; filename: string;
@ -125,9 +125,9 @@ export interface KoboldConfig {
} }
export interface KoboldAPI { export interface KoboldAPI {
getInstalledVersions: () => Promise<InstalledVersion[]>; getInstalledBackends: () => Promise<InstalledBackend[]>;
getCurrentVersion: () => Promise<InstalledVersion | null>; getCurrentBackend: () => Promise<InstalledBackend | null>;
setCurrentVersion: (version: string) => Promise<boolean>; setCurrentBackend: (version: string) => Promise<boolean>;
getPlatform: () => Promise<string>; getPlatform: () => Promise<string>;
detectGPU: () => Promise<BasicGPUInfo>; detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
@ -135,8 +135,10 @@ export interface KoboldAPI {
detectGPUMemory: () => Promise<GPUMemoryInfo[]>; detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
detectSystemMemory: () => Promise<SystemMemoryInfo>; detectSystemMemory: () => Promise<SystemMemoryInfo>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectBackendSupport: () => Promise<BackendSupport | null>; detectAccelerationSupport: () => Promise<AccelerationSupport | null>;
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>; getAvailableAccelerations: (
includeDisabled?: boolean
) => Promise<AccelerationOption[]>;
getCurrentInstallDir: () => Promise<string>; getCurrentInstallDir: () => Promise<string>;
selectInstallDirectory: () => Promise<string | null>; selectInstallDirectory: () => Promise<string | null>;
downloadRelease: ( downloadRelease: (

12
src/types/index.d.ts vendored
View file

@ -66,14 +66,6 @@ export interface UpdateInfo {
hasUpdate: boolean; hasUpdate: boolean;
} }
export interface InstalledVersion {
version: string;
path: string;
filename: string;
size?: number;
actualVersion?: string;
}
export interface VersionInfo { export interface VersionInfo {
name: string; name: string;
version: string; version: string;
@ -97,12 +89,12 @@ export interface SelectOption {
label: string; label: string;
} }
export interface BackendOption extends SelectOption { export interface AccelerationOption extends SelectOption {
readonly devices?: readonly (string | GPUDevice)[]; readonly devices?: readonly (string | GPUDevice)[];
readonly disabled?: boolean; readonly disabled?: boolean;
} }
export interface BackendSupport { export interface AccelerationSupport {
rocm: boolean; rocm: boolean;
vulkan: boolean; vulkan: boolean;
clblast: boolean; clblast: boolean;

View file

@ -1,16 +1,17 @@
import type { InstalledVersion } from '@/types'; import type { InstalledBackend } from '@/types/electron';
export const getDisplayNameFromPath = (installedVersion: InstalledVersion) => { export const getDisplayNameFromPath = (installedBackend: InstalledBackend) => {
const pathParts = installedVersion.path.split(/[/\\]/); const pathParts = installedBackend.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex( const launcherIndex = pathParts.findIndex(
(part) => part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe' (part: string) =>
part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
); );
if (launcherIndex > 0) { if (launcherIndex > 0) {
return stripVersionSuffix(pathParts[launcherIndex - 1]); return stripVersionSuffix(pathParts[launcherIndex - 1]);
} }
return installedVersion.filename; return installedBackend.filename;
}; };
export const stripAssetExtensions = (assetName: string) => export const stripAssetExtensions = (assetName: string) =>

View file

@ -3736,7 +3736,7 @@ __metadata:
jiti: "npm:^2.6.1" jiti: "npm:^2.6.1"
lucide-react: "npm:^0.555.0" lucide-react: "npm:^0.555.0"
mime-types: "npm:^3.0.2" mime-types: "npm:^3.0.2"
prettier: "npm:^3.7.1" prettier: "npm:^3.7.2"
react: "npm:^19.2.0" react: "npm:^19.2.0"
react-dom: "npm:^19.2.0" react-dom: "npm:^19.2.0"
react-error-boundary: "npm:^6.0.0" react-error-boundary: "npm:^6.0.0"
@ -5506,12 +5506,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^3.7.1": "prettier@npm:^3.7.2":
version: 3.7.1 version: 3.7.2
resolution: "prettier@npm:3.7.1" resolution: "prettier@npm:3.7.2"
bin: bin:
prettier: bin/prettier.cjs prettier: bin/prettier.cjs
checksum: 10c0/a6610043ee0a64a3251a948bf82fad3e59d984a8e8dea206400cfa190585417e3343b32c1f6ae7d8f40798a9b4bd91affc08fa7795dd99a9dec5c9bccdf31500 checksum: 10c0/df3d658df301face0918f8ecbd4354f32e1151d83a3a4720c7f252342baf631466568f708e0e57beea55bbc56415c40208adc76a91d5f1a88f3e743d0d775dc0
languageName: node languageName: node
linkType: hard linkType: hard