Enhance local provider integration and setup instructions

- Introduced separate handling for local providers: Ollama, MLX, and Apple Foundation Models.
- Updated settings to include distinct sections for each local provider with relevant instructions and model filters.
- Improved user interface for selecting local providers based on the operating system.
- Refactored provider names to enhance clarity and maintainability in the codebase.
This commit is contained in:
Jérôme Commaret 2026-05-20 23:29:12 +02:00
parent 8c9b8e43ce
commit 81bd697dcb
3 changed files with 142 additions and 28 deletions

View file

@ -6,9 +6,9 @@
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, ollamaProviderNames, mlxProviderNames, appleProviderNames, otherLocalProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
import { os } from '../../../../common/helpers/systemInfo.js';
import { OllamaSetupInstructions, MlxSetupInstructions, AppleFoundationModelsSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
import { isLinux } from '../../../../../../../base/common/platform.js';
@ -99,6 +99,22 @@ const tabNames = ['Free', 'Paid', 'Local'] as const;
type TabName = typeof tabNames[number] | 'Cloud/Other';
type LocalSubTab = 'ollama' | 'mlx' | 'apple' | 'other';
const localSubTabLabels: Record<LocalSubTab, string> = {
ollama: 'Ollama',
mlx: 'MLX',
apple: 'apple',
other: 'Other',
};
const providerNamesOfLocalSubTab: Record<LocalSubTab, ProviderName[]> = {
ollama: [...ollamaProviderNames],
mlx: [...mlxProviderNames],
apple: [...appleProviderNames],
other: [...otherLocalProviderNames],
};
// Data for cloud providers tab
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
@ -128,6 +144,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
const [currentTab, setCurrentTab] = useState<TabName>('Free');
const [localSubTab, setLocalSubTab] = useState<LocalSubTab>('ollama');
const settingsState = useSettingsState();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@ -200,7 +217,24 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">{descriptionOfTab[currentTab]}</div>
</div>
{providerNamesOfTab[currentTab].map((providerName) => (
{currentTab === 'Local' && (
<div className="flex gap-2 mb-6 w-full max-w-xl flex-wrap">
{(os === 'mac' ? (['ollama', 'mlx', 'apple', 'other'] as const) : (['ollama', 'other'] as const)).map(sub => (
<button
key={sub}
className={`py-1.5 px-3 rounded-md text-sm ${localSubTab === sub
? 'bg-[#0e70c0]/80 text-white'
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
}`}
onClick={() => setLocalSubTab(sub)}
>
{localSubTabLabels[sub]}
</button>
))}
</div>
)}
{(currentTab === 'Local' ? providerNamesOfLocalSubTab[localSubTab] : providerNamesOfTab[currentTab]).map((providerName) => (
<div key={providerName} className="w-full max-w-xl mb-10">
<div className="text-xl mb-2">
Add {displayInfoOfProviderName(providerName).title}
@ -226,6 +260,8 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
</div>
{providerName === 'ollama' && <OllamaSetupInstructions />}
{providerName === 'mlx' && os === 'mac' && <MlxSetupInstructions />}
{providerName === 'appleFoundationModels' && os === 'mac' && <AppleFoundationModelsSetupInstructions />}
</div>
))}
@ -239,7 +275,7 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">Local models should be detected automatically. You can add custom models below.</div>
)}
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
{currentTab === 'Local' && <ModelDump filteredProviders={[...providerNamesOfLocalSubTab[localSubTab]]} />}
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
</div>
)}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; // Added useRef import just in case it was missed, though likely already present
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, ollamaProviderNames, mlxProviderNames, appleProviderNames, otherLocalProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import { os } from '../../../../common/helpers/systemInfo.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
@ -26,7 +26,10 @@ import { StorageScope, StorageTarget } from '../../../../../../../platform/stora
type Tab =
| 'models'
| 'localProviders'
| 'ollama'
| 'mlx'
| 'apple'
| 'localOther'
| 'providers'
| 'featureOptions'
| 'mcp'
@ -95,11 +98,11 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
/>
}
const RefreshableModels = () => {
const RefreshableModels = ({ providerNamesFilter }: { providerNamesFilter?: readonly ProviderName[] }) => {
const settingsState = useSettingsState()
const names = providerNamesFilter ?? refreshableProviderNames
const buttons = refreshableProviderNames.map(providerName => {
const buttons = names.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
return <RefreshModelButton key={providerName} providerName={providerName} />
})
@ -746,7 +749,7 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
}
export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => {
export const VoidProviderSettings = ({ providerNames }: { providerNames: readonly ProviderName[] }) => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} showProviderTitle={true} showProviderSuggestions={true} />
@ -754,6 +757,32 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
</>
}
const LocalProviderSection = ({ title, description, instructions, providerNames, refreshable, autoSetup, modelFilter }: {
title: string
description?: string
instructions?: React.ReactNode
providerNames: readonly ProviderName[]
refreshable?: boolean
autoSetup?: React.ReactNode
modelFilter?: readonly ProviderName[]
}) => (
<div className='flex flex-col gap-4 mb-12'>
<h2 className='text-3xl mb-1'>{title}</h2>
{description && <h3 className='text-void-fg-3 mb-2'>{description}</h3>}
{instructions && <div className='opacity-80 mb-2'>{instructions}</div>}
{autoSetup}
{refreshable && <RefreshableModels providerNamesFilter={providerNames} />}
<VoidProviderSettings providerNames={providerNames} />
{modelFilter && (
<>
<div className='w-full h-[1px] my-2' />
<h3 className='text-lg text-void-fg-3 mb-2'>Models for {title}</h3>
<ModelDump filteredProviders={[...modelFilter]} />
</>
)}
</div>
)
type TabName = 'models' | 'general'
export const AutoDetectLocalModelsToggle = () => {
@ -1110,7 +1139,12 @@ export const Settings = () => {
const navItems: { tab: Tab; label: string }[] = [
{ tab: 'models', label: 'Models' },
{ tab: 'localProviders', label: 'Local Providers' },
{ tab: 'ollama', label: 'Ollama' },
...(os === 'mac' ? ([
{ tab: 'mlx' as const, label: 'MLX' },
{ tab: 'apple' as const, label: 'apple' },
]) : []),
{ tab: 'localOther', label: 'Local (other)' },
{ tab: 'providers', label: 'Main Providers' },
{ tab: 'featureOptions', label: 'Feature Options' },
{ tab: 'general', label: 'General' },
@ -1254,27 +1288,65 @@ export const Settings = () => {
<ModelDump />
<div className='w-full h-[1px] my-4' />
<AutoDetectLocalModelsToggle />
<AutoSetupMlxToggle />
<AutoSetupAppleFoundationModelsToggle />
<RefreshableModels />
<div className='w-full h-[1px] my-4' />
<p className='text-void-fg-3 text-sm mb-4'>Per-provider setup and refresh are under <strong>Ollama</strong>, <strong>MLX</strong>, and <strong>apple</strong> in the sidebar.</p>
</ErrorBoundary>
</div>
{/* Local Providers section */}
<div className={shouldShowTab('localProviders') ? `` : 'hidden'}>
<div className={shouldShowTab('ollama') ? `` : 'hidden'}>
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<LocalProviderSection
title='Ollama'
description='Pull models with `ollama pull` — Void autodetects them at your endpoint.'
instructions={<OllamaSetupInstructions sayWeAutoDetect={true} />}
providerNames={ollamaProviderNames}
refreshable
modelFilter={ollamaProviderNames}
/>
</ErrorBoundary>
</div>
<div className='opacity-80 mb-4'>
<OllamaSetupInstructions sayWeAutoDetect={true} />
</div>
<div className='opacity-80'>
<MlxSetupInstructions />
<AppleFoundationModelsSetupInstructions />
</div>
{os === 'mac' && (
<div className={shouldShowTab('mlx') ? `` : 'hidden'}>
<ErrorBoundary>
<LocalProviderSection
title='MLX'
description='One loaded model at a time via mlx_lm.server (port 8080 by default).'
instructions={<MlxSetupInstructions />}
providerNames={mlxProviderNames}
refreshable
autoSetup={<AutoSetupMlxToggle />}
modelFilter={mlxProviderNames}
/>
</ErrorBoundary>
</div>
)}
<VoidProviderSettings providerNames={localProviderNames} />
{os === 'mac' && (
<div className={shouldShowTab('apple') ? `` : 'hidden'}>
<ErrorBoundary>
<LocalProviderSection
title='apple'
description='On-device Foundation model via maclocal-api (`afm`, port 9999).'
instructions={<AppleFoundationModelsSetupInstructions />}
providerNames={appleProviderNames}
refreshable
autoSetup={<AutoSetupAppleFoundationModelsToggle />}
modelFilter={appleProviderNames}
/>
</ErrorBoundary>
</div>
)}
<div className={shouldShowTab('localOther') ? `` : 'hidden'}>
<ErrorBoundary>
<LocalProviderSection
title='Local (other)'
description='vLLM and LM Studio — OpenAI-compatible local servers.'
providerNames={otherLocalProviderNames}
refreshable
modelFilter={otherLocalProviderNames}
/>
</ErrorBoundary>
</div>

View file

@ -16,7 +16,13 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama', 'vLLM', 'lmStudio', 'mlx', 'appleFoundationModels'] satisfies ProviderName[] // all local names
export const ollamaProviderNames = ['ollama'] as const satisfies ProviderName[]
export const mlxProviderNames = ['mlx'] as const satisfies ProviderName[]
export const appleProviderNames = ['appleFoundationModels'] as const satisfies ProviderName[]
/** vLLM, LM Studio — separate from Ollama / MLX / apple */
export const otherLocalProviderNames = ['vLLM', 'lmStudio'] as const satisfies ProviderName[]
export const localProviderNames = [...ollamaProviderNames, ...mlxProviderNames, ...appleProviderNames, ...otherLocalProviderNames] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>