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:
Egor 2025-11-28 23:33:37 -08:00
parent 421478778b
commit 4b2e9b2ae9
28 changed files with 677 additions and 652 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
import { Text, Group, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react';
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 { useLaunchConfig } from '@/hooks/useLaunchConfig';
import type { BackendOption } from '@/types';
import type { AccelerationOption } from '@/types';
import { Select } from '@/components/Select';
export const BackendSelector = () => {
export const AccelerationSelector = () => {
const {
backend,
gpuLayers,
@ -21,60 +21,67 @@ export const BackendSelector = () => {
handleAutoGpuLayersChange,
} = useLaunchConfig();
const [availableBackends, setAvailableBackends] = useState<BackendOption[]>(
[]
);
const [isLoadingBackends, setIsLoadingBackends] = useState(false);
const [availableAccelerations, setAvailableAccelerations] = useState<
AccelerationOption[]
>([]);
const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false);
const [isCalculatingLayers, setIsCalculatingLayers] = useState(false);
const [isMac, setIsMac] = useState(false);
const hasInitialized = useRef(false);
useEffect(() => {
const loadBackends = async () => {
setIsLoadingBackends(true);
const loadAccelerations = async () => {
setIsLoadingAccelerations(true);
const backends =
await window.electronAPI.kobold.getAvailableBackends(true);
const [accelerations, platform] = await Promise.all([
window.electronAPI.kobold.getAvailableAccelerations(true),
window.electronAPI.kobold.getPlatform(),
]);
setAvailableBackends(backends || []);
setIsLoadingBackends(false);
setAvailableAccelerations(accelerations || []);
setIsMac(platform === 'darwin');
setIsLoadingAccelerations(false);
hasInitialized.current = true;
};
if (!hasInitialized.current) {
loadBackends();
loadAccelerations();
}
const cleanup = window.electronAPI.kobold.onVersionsUpdated(() => {
hasInitialized.current = false;
loadBackends();
loadAccelerations();
});
return cleanup;
}, []);
useEffect(() => {
if (availableBackends.length > 0 && backend) {
const isBackendAvailable = availableBackends.some(
(b) => b.value === backend && !b.disabled
if (availableAccelerations.length > 0 && backend) {
const isAccelerationAvailable = availableAccelerations.some(
(a) => a.value === backend && !a.disabled
);
if (!isBackendAvailable) {
const fallbackBackend = availableBackends.find((b) => !b.disabled);
if (fallbackBackend) {
handleBackendChange(fallbackBackend.value);
if (!isAccelerationAvailable) {
const fallbackAcceleration = availableAccelerations.find(
(a) => !a.disabled
);
if (fallbackAcceleration) {
handleBackendChange(fallbackAcceleration.value);
}
}
}
}, [availableBackends, backend, handleBackendChange]);
}, [availableAccelerations, backend, handleBackendChange]);
useEffect(() => {
const calculateLayers = async () => {
const isCpuOnly = backend === 'cpu' && !isMac;
if (
!autoGpuLayers ||
!model ||
!contextSize ||
backend === 'cpu' ||
isLoadingBackends
isCpuOnly ||
isLoadingAccelerations
) {
return;
}
@ -133,7 +140,8 @@ export const BackendSelector = () => {
backend,
gpuDeviceSelection,
flashattention,
isLoadingBackends,
isLoadingAccelerations,
isMac,
handleGpuLayersChange,
]);
@ -143,16 +151,20 @@ export const BackendSelector = () => {
<div style={{ flex: 1, marginRight: '1rem' }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
Backend
Acceleration
</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>
<Select
placeholder={
isLoadingBackends ? 'Loading backends...' : 'Select backend'
isLoadingAccelerations
? 'Loading accelerations...'
: 'Select acceleration'
}
value={
availableBackends.some((b) => b.value === backend && !b.disabled)
availableAccelerations.some(
(a) => a.value === backend && !a.disabled
)
? backend
: null
}
@ -161,22 +173,24 @@ export const BackendSelector = () => {
handleBackendChange(value);
}
}}
data={availableBackends.map((b) => ({
value: b.value,
label: b.label,
disabled: b.disabled,
data={availableAccelerations.map((a) => ({
value: a.value,
label: a.label,
disabled: a.disabled,
}))}
disabled={isLoadingBackends || availableBackends.length === 0}
disabled={
isLoadingAccelerations || availableAccelerations.length === 0
}
renderOption={({ option }) => {
const backendData = availableBackends.find(
(b) => b.value === option.value
const accelerationData = availableAccelerations.find(
(a) => a.value === option.value
);
return (
<BackendSelectItem
label={backendData?.label || option.label.split(' (')[0]}
devices={backendData?.devices}
disabled={backendData?.disabled}
<AccelerationSelectItem
label={accelerationData?.label || option.label.split(' (')[0]}
devices={accelerationData?.devices}
disabled={accelerationData?.disabled}
/>
);
}}
@ -209,7 +223,7 @@ export const BackendSelector = () => {
step={1}
size="sm"
w={80}
disabled={autoGpuLayers || backend === 'cpu'}
disabled={autoGpuLayers || (backend === 'cpu' && !isMac)}
/>
<Group gap="xs" align="center">
<Checkbox
@ -219,7 +233,7 @@ export const BackendSelector = () => {
handleAutoGpuLayersChange(event.currentTarget.checked)
}
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." />
</Group>
@ -227,7 +241,7 @@ export const BackendSelector = () => {
</div>
</Group>
<GpuDeviceSelector availableBackends={availableBackends} />
<GpuDeviceSelector availableAccelerations={availableAccelerations} />
</div>
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ export const SystemTab = () => {
const loadKoboldVersion = async () => {
const currentVersion =
await window.electronAPI.kobold.getCurrentVersion();
await window.electronAPI.kobold.getCurrentBackend();
if (currentVersion) {
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[] = [];
if (!launchArgs.backend || launchArgs.backend === 'cpu') {
if (launchArgs.backend === 'cpu') {
if (launchArgs.backend === 'cpu' && platform !== 'darwin') {
args.push('--usecpu');
}
return args;
@ -271,10 +271,12 @@ export const useLaunchLogic = ({
onLaunch();
const platform = await window.electronAPI.kobold.getPlatform();
const args: string[] = [
...buildModelArgs(model, sdmodel, launchArgs),
...buildConfigArgs(hasImageModel, launchArgs),
...buildBackendArgs(launchArgs),
...buildBackendArgs(launchArgs, platform),
];
if (launchArgs.additionalArguments.trim()) {

View file

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

View file

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

View file

@ -8,11 +8,11 @@ import {
} from '@/main/modules/koboldcpp/launcher';
import { downloadRelease } from '@/main/modules/koboldcpp/download';
import {
getInstalledVersions,
getCurrentVersion,
setCurrentVersion,
getInstalledBackends,
getCurrentBackend,
setCurrentBackend,
deleteRelease,
} from '@/main/modules/koboldcpp/version';
} from '@/main/modules/koboldcpp/backend';
import {
getConfigFiles,
saveConfigFile,
@ -61,9 +61,9 @@ import {
detectSystemMemory,
} from '@/main/modules/hardware';
import {
detectBackendSupport,
getAvailableBackends,
} from '@/main/modules/koboldcpp/backend';
detectAccelerationSupport,
getAvailableAccelerations,
} from '@/main/modules/koboldcpp/acceleration';
import {
openPerformanceManager,
startMonitoring,
@ -85,9 +85,9 @@ export function setupIPCHandlers() {
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());
@ -105,8 +105,8 @@ export function setupIPCHandlers() {
setConfig('selectedConfig', configName)
);
ipcMain.handle('kobold:setCurrentVersion', (_, version) =>
setCurrentVersion(version)
ipcMain.handle('kobold:setCurrentBackend', (_, version) =>
setCurrentBackend(version)
);
ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir());
@ -127,10 +127,13 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:detectROCm', () => detectROCm());
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport());
ipcMain.handle('kobold:detectAccelerationSupport', () =>
detectAccelerationSupport()
);
ipcMain.handle('kobold:getAvailableBackends', (_, includeDisabled = false) =>
getAvailableBackends(includeDisabled)
ipcMain.handle(
'kobold:getAvailableAccelerations',
(_, includeDisabled = false) => getAvailableAccelerations(includeDisabled)
);
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 { platform } from 'process';
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 { getCurrentBinaryInfo } from './version';
import { detectGPUCapabilities, detectCPU } from '../hardware';
import { tryExecute, safeExecute } from '@/utils/node/logging';
import type { BackendOption, BackendSupport } from '@/types';
import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path';
import type { InstalledBackend } from '@/types/electron';
const backendSupportCache = new Map<string, BackendSupport>();
const availableBackendsCache = new Map<string, BackendOption[]>();
const versionCache = new Map<
string,
{ version: string; actualVersion?: string } | null
>();
async function detectBackendSupportFromPath(koboldBinaryPath: string) {
if (backendSupportCache.has(koboldBinaryPath)) {
return backendSupportCache.get(koboldBinaryPath)!;
export function clearVersionCache(path?: string) {
if (path) {
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 () =>
(await safeExecute(async () => {
const currentBinaryInfo = await getCurrentBinaryInfo();
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) {
export async function getInstalledBackends() {
try {
const installDir = getInstallDir();
if (!(await pathExists(installDir))) {
return [];
}
const backends: BackendOption[] = [];
const items = await readdir(installDir);
const launchers: { path: string; filename: string; size: number }[] = [];
if (backendSupport.cuda) {
const isSupported = hardwareCapabilities.cuda.devices.length > 0;
if (isSupported || includeDisabled) {
backends.push({
value: 'cuda',
label: 'CUDA',
devices: hardwareCapabilities.cuda.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
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,
});
}
}
}
if (backendSupport.rocm) {
const isSupported = hardwareCapabilities.rocm.devices.length > 0;
if (isSupported || includeDisabled) {
backends.push({
value: 'rocm',
label: 'ROCm',
devices: hardwareCapabilities.rocm.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
const versionPromises = launchers.map(async (launcher) => {
try {
const versionInfo = await getVersionFromBinary(launcher.path);
if (backendSupport.vulkan) {
const isSupported = hardwareCapabilities.vulkan.devices.length > 0;
if (isSupported || includeDisabled) {
backends.push({
value: 'vulkan',
label: 'Vulkan',
devices: hardwareCapabilities.vulkan.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
}
}
if (!versionInfo) {
return null;
}
if (backendSupport.clblast) {
const discreteDevices = hardwareCapabilities.clblast.devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated
);
const isSupported = discreteDevices.length > 0;
if (isSupported || includeDisabled) {
backends.push({
value: 'clblast',
label: 'CLBlast',
devices: hardwareCapabilities.clblast.devices,
disabled: includeDisabled ? !isSupported : undefined,
});
return {
version: versionInfo.version,
path: launcher.path,
filename: launcher.filename,
size: launcher.size,
actualVersion: versionInfo.actualVersion,
} as InstalledBackend;
} catch (error) {
logError(
`Could not detect version for ${launcher.filename}:`,
error as Error
);
return null;
}
}
backends.push({
value: 'cpu',
label: 'CPU',
devices: cpuCapabilities?.devices.map((device) => device.name) || [],
disabled: false,
});
if (includeDisabled) {
backends.sort((a, b) => {
if (a.disabled === b.disabled) return 0;
return a.disabled ? 1 : -1;
});
const results = await Promise.all(versionPromises);
return results.filter(
(version): version is InstalledBackend => version !== null
);
} 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);
return backends;
}, 'Failed to get available backends');
const currentBinaryPath = getCurrentKoboldBinary();
if (currentBinaryPath === binaryPath) {
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 { getLauncherPath } from '@/utils/node/path';
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
import { clearVersionCache } from './version';
import { clearVersionCache } from './backend';
async function removeDirectoryWithRetry(
dirPath: string,

View file

@ -7,7 +7,7 @@ import { sendKoboldOutput } from '@/main/modules/window';
import { SERVER_READY_SIGNALS } from '@/constants';
import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold';
import { getCurrentVersion } from '../version';
import { getCurrentBackend } from '../backend';
import {
getCurrentKoboldBinary,
get as getConfig,
@ -153,15 +153,15 @@ export async function launchKoboldCpp(
spawnPreLaunchCommands(preLaunchCommands);
}
const currentVersion = await getCurrentVersion();
if (!currentVersion || !(await pathExists(currentVersion.path))) {
const currentBackend = await getCurrentBackend();
if (!currentBackend || !(await pathExists(currentBackend.path))) {
const rawPath = getCurrentKoboldBinary();
const error = currentVersion
? `Binary file does not exist at path: ${currentVersion.path}`
: 'No version configured';
const error = currentBackend
? `Binary file does not exist at path: ${currentBackend.path}`
: 'No backend configured';
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 {
@ -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);
@ -191,14 +191,14 @@ export async function launchKoboldCpp(
await startProxy(koboldHost, koboldPort);
const child = spawn(currentVersion.path, finalArgs, {
const child = spawn(currentBackend.path, finalArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
koboldProcess = child;
const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`;
const commandLine = `${currentBackend.path} ${finalArgs.join(' ')}`;
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';
const koboldAPI: KoboldAPI = {
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
setCurrentVersion: (version) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version),
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
setCurrentBackend: (version) =>
ipcRenderer.invoke('kobold:setCurrentBackend', version),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
@ -28,9 +28,10 @@ const koboldAPI: KoboldAPI = {
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
getAvailableBackends: (includeDisabled) =>
ipcRenderer.invoke('kobold:getAvailableBackends', includeDisabled),
detectAccelerationSupport: () =>
ipcRenderer.invoke('kobold:detectAccelerationSupport'),
getAvailableAccelerations: (includeDisabled) =>
ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'),

View file

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

View file

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

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

@ -66,14 +66,6 @@ export interface UpdateInfo {
hasUpdate: boolean;
}
export interface InstalledVersion {
version: string;
path: string;
filename: string;
size?: number;
actualVersion?: string;
}
export interface VersionInfo {
name: string;
version: string;
@ -97,12 +89,12 @@ export interface SelectOption {
label: string;
}
export interface BackendOption extends SelectOption {
export interface AccelerationOption extends SelectOption {
readonly devices?: readonly (string | GPUDevice)[];
readonly disabled?: boolean;
}
export interface BackendSupport {
export interface AccelerationSupport {
rocm: boolean;
vulkan: 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) => {
const pathParts = installedVersion.path.split(/[/\\]/);
export const getDisplayNameFromPath = (installedBackend: InstalledBackend) => {
const pathParts = installedBackend.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) => part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
(part: string) =>
part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
);
if (launcherIndex > 0) {
return stripVersionSuffix(pathParts[launcherIndex - 1]);
}
return installedVersion.filename;
return installedBackend.filename;
};
export const stripAssetExtensions = (assetName: string) =>

View file

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