mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
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:
parent
421478778b
commit
4b2e9b2ae9
28 changed files with 677 additions and 652 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
184
src/main/modules/koboldcpp/acceleration.ts
Normal file
184
src/main/modules/koboldcpp/acceleration.ts
Normal 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 }];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
|||
18
src/types/electron.d.ts
vendored
18
src/types/electron.d.ts
vendored
|
|
@ -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
12
src/types/index.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
10
yarn.lock
10
yarn.lock
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue