mirror of
https://github.com/voideditor/void
synced 2026-05-22 08:58:26 +00:00
Add support for MLX and Apple Foundation Models, including auto-setup options and model handling in settings
This commit is contained in:
parent
98baa32c41
commit
a5ed7b6af7
18 changed files with 912 additions and 15 deletions
|
|
@ -131,6 +131,10 @@ import { MetricsMainService } from '../../workbench/contrib/void/electron-main/m
|
|||
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
|
||||
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
|
||||
import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
|
||||
import { AppleFoundationModelsMainService } from '../../workbench/contrib/void/electron-main/appleFoundationModelsMainService.js';
|
||||
import { IAppleFoundationModelsMainService } from '../../workbench/contrib/void/common/appleFoundationModelsTypes.js';
|
||||
import { MlxMainService } from '../../workbench/contrib/void/electron-main/mlxMainService.js';
|
||||
import { IMlxMainService } from '../../workbench/contrib/void/common/mlxTypes.js';
|
||||
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
/**
|
||||
|
|
@ -1105,6 +1109,8 @@ export class CodeApplication extends Disposable {
|
|||
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false));
|
||||
services.set(IAppleFoundationModelsMainService, new SyncDescriptor(AppleFoundationModelsMainService, undefined, false));
|
||||
services.set(IMlxMainService, new SyncDescriptor(MlxMainService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
|
|
@ -1250,6 +1256,12 @@ export class CodeApplication extends Disposable {
|
|||
const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel);
|
||||
|
||||
const appleFoundationModelsChannel = ProxyChannel.fromService(accessor.get(IAppleFoundationModelsMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-appleFoundationModels', appleFoundationModelsChannel);
|
||||
|
||||
const mlxChannel = ProxyChannel.fromService(accessor.get(IMlxMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-mlx', mlxChannel);
|
||||
|
||||
// Void added this
|
||||
const mcpChannel = new MCPChannel();
|
||||
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { APPLE_FOUNDATION_MODELS_DEFAULT_ENDPOINT } from '../common/appleFoundationModelsTypes.js';
|
||||
import { IAppleFoundationModelsService } from '../common/appleFoundationModelsService.js';
|
||||
import { IRefreshModelService } from '../common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
|
||||
export class AppleFoundationModelsWorkbenchContrib extends Disposable {
|
||||
static readonly ID = 'workbench.contrib.appleFoundationModels';
|
||||
|
||||
private _didRun = false;
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IAppleFoundationModelsService private readonly appleFoundationModelsService: IAppleFoundationModelsService,
|
||||
@IRefreshModelService private readonly refreshModelService: IRefreshModelService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.voidSettingsService.waitForInitState.then(() => this._maybeBootstrap());
|
||||
this._register(this.voidSettingsService.onDidChangeState((e) => {
|
||||
if (typeof e === 'object' && e[1] === 'autoSetupAppleFoundationModels') {
|
||||
this._didRun = false;
|
||||
this._maybeBootstrap();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async _maybeBootstrap(): Promise<void> {
|
||||
if (this._didRun) {
|
||||
return;
|
||||
}
|
||||
if (!isMacintosh) {
|
||||
return;
|
||||
}
|
||||
if (!this.voidSettingsService.state.globalSettings.autoSetupAppleFoundationModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._didRun = true;
|
||||
|
||||
const result = await this.appleFoundationModelsService.ensureReady({
|
||||
installIfMissing: true,
|
||||
startServer: true,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const endpoint = result.endpoint || APPLE_FOUNDATION_MODELS_DEFAULT_ENDPOINT;
|
||||
if (this.voidSettingsService.state.settingsOfProvider.appleFoundationModels.endpoint !== endpoint) {
|
||||
await this.voidSettingsService.setSettingOfProvider('appleFoundationModels', 'endpoint', endpoint);
|
||||
}
|
||||
this.refreshModelService.startRefreshingModels('appleFoundationModels', { enableProviderOnSuccess: true, doNotFire: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = result.errorMessage ?? result.log.join('\n');
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: 'Apple: automatic setup failed',
|
||||
source: detail || 'Void',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(AppleFoundationModelsWorkbenchContrib.ID, AppleFoundationModelsWorkbenchContrib, WorkbenchPhase.AfterRestored);
|
||||
73
src/vs/workbench/contrib/void/browser/mlxWorkbenchContrib.ts
Normal file
73
src/vs/workbench/contrib/void/browser/mlxWorkbenchContrib.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
|
||||
import { MLX_DEFAULT_ENDPOINT } from '../common/mlxTypes.js';
|
||||
import { IMlxService } from '../common/mlxService.js';
|
||||
import { IRefreshModelService } from '../common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
|
||||
export class MlxWorkbenchContrib extends Disposable {
|
||||
static readonly ID = 'workbench.contrib.mlx';
|
||||
|
||||
private _didRun = false;
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IMlxService private readonly mlxService: IMlxService,
|
||||
@IRefreshModelService private readonly refreshModelService: IRefreshModelService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.voidSettingsService.waitForInitState.then(() => this._maybeBootstrap());
|
||||
this._register(this.voidSettingsService.onDidChangeState((e) => {
|
||||
if (typeof e === 'object' && e[1] === 'autoSetupMlx') {
|
||||
this._didRun = false;
|
||||
this._maybeBootstrap();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async _maybeBootstrap(): Promise<void> {
|
||||
if (this._didRun) {
|
||||
return;
|
||||
}
|
||||
if (!isMacintosh) {
|
||||
return;
|
||||
}
|
||||
if (!this.voidSettingsService.state.globalSettings.autoSetupMlx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._didRun = true;
|
||||
|
||||
const result = await this.mlxService.ensureReady({
|
||||
installIfMissing: true,
|
||||
startServer: true,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const endpoint = result.endpoint || MLX_DEFAULT_ENDPOINT;
|
||||
if (this.voidSettingsService.state.settingsOfProvider.mlx.endpoint !== endpoint) {
|
||||
await this.voidSettingsService.setSettingOfProvider('mlx', 'endpoint', endpoint);
|
||||
}
|
||||
this.refreshModelService.startRefreshingModels('mlx', { enableProviderOnSuccess: true, doNotFire: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = result.errorMessage ?? result.log.join('\n');
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Warning,
|
||||
message: 'MLX: automatic setup failed',
|
||||
source: detail || 'Void',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(MlxWorkbenchContrib.ID, MlxWorkbenchContrib, WorkbenchPhase.AfterRestored);
|
||||
|
|
@ -511,8 +511,8 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio', 'mlx', 'appleFoundationModels'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM', 'mlx'],
|
||||
all: providerNames,
|
||||
}
|
||||
|
||||
|
|
@ -564,7 +564,7 @@ const VoidOnboardingContent = () => {
|
|||
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
|
||||
smart: "Most intelligent and best for agent mode.",
|
||||
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
|
||||
cheap: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
|
||||
cheap: "Use great deals like Gemini 2.5 Pro, or self-host with Ollama, vLLM, or MLX on Apple Silicon.",
|
||||
all: "",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,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 { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useIsOptedOut, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
|
|
@ -13,7 +14,6 @@ import { URI } from '../../../../../../../base/common/uri.js'
|
|||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
|
||||
import Severity from '../../../../../../../base/common/severity.js'
|
||||
|
|
@ -737,7 +737,9 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
|
|||
{showProviderSuggestions && needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox className="pl-2 mb-4" text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox className="pl-2 mb-4" text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
: (providerName === 'mlx' || providerName === 'appleFoundationModels' || providerName === 'lmStudio' || providerName === 'vLLM') ?
|
||||
<WarningBox className="pl-2 mb-4" text={`Start your local server — Void will auto-detect models at the endpoint.`} />
|
||||
: <WarningBox className="pl-2 mb-4" text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
|
|
@ -781,6 +783,56 @@ export const AutoDetectLocalModelsToggle = () => {
|
|||
|
||||
}
|
||||
|
||||
export const AutoSetupMlxToggle = () => {
|
||||
if (os !== 'mac') {
|
||||
return null
|
||||
}
|
||||
|
||||
const settingName: GlobalSettingName = 'autoSetupMlx'
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const enabled = voidSettingsState.globalSettings[settingName]
|
||||
|
||||
return <ButtonLeftTextRightOption
|
||||
leftButton={<VoidSwitch
|
||||
size='xxs'
|
||||
value={enabled}
|
||||
onChange={(newVal) => {
|
||||
voidSettingsService.setGlobalSetting(settingName, newVal)
|
||||
metricsService.capture('Click', { action: 'MLX Auto-Setup Toggle', settingName, enabled: newVal })
|
||||
}}
|
||||
/>}
|
||||
text='On macOS: install mlx-lm (pip) if needed, start mlx_lm.server, and autodetect the loaded model.'
|
||||
/>
|
||||
}
|
||||
|
||||
export const AutoSetupAppleFoundationModelsToggle = () => {
|
||||
if (os !== 'mac') {
|
||||
return null
|
||||
}
|
||||
|
||||
const settingName: GlobalSettingName = 'autoSetupAppleFoundationModels'
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
const enabled = voidSettingsState.globalSettings[settingName]
|
||||
|
||||
return <ButtonLeftTextRightOption
|
||||
leftButton={<VoidSwitch
|
||||
size='xxs'
|
||||
value={enabled}
|
||||
onChange={(newVal) => {
|
||||
voidSettingsService.setGlobalSetting(settingName, newVal)
|
||||
metricsService.capture('Click', { action: 'Apple FM Auto-Setup Toggle', settingName, enabled: newVal })
|
||||
}}
|
||||
/>}
|
||||
text='On macOS: install `afm` (Homebrew) if needed, start the Apple server, and enable the `foundation` model.'
|
||||
/>
|
||||
}
|
||||
|
||||
export const AIInstructionsBox = () => {
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
|
@ -835,6 +887,27 @@ export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?:
|
|||
</div>
|
||||
}
|
||||
|
||||
export const MlxSetupInstructions = () => {
|
||||
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 text-void-fg-3 text-sm list-decimal select-text mb-4'>
|
||||
<div><ChatMarkdownRender string={`MLX (one model at a time)`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`1. Void can run \`pip install mlx-lm\` and start \`mlx_lm.server\` for you (toggle in Settings → Models). Default model: \`mlx-community/Qwen2.5-Coder-1.5B-Instruct\` on port 8080.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`2. Void shows only **one** autodetected entry: the model currently loaded by the server.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`3. **Switch models**: stop the server, run \`mlx_lm.server --model <other-hf-repo>\`, then click **Refresh** next to MLX in Settings → Models.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`4. **Second model in parallel**: run another server on a different port (e.g. \`mlx_lm.server --model … --port 8081\`), then at the bottom of the model list use **Add Model** → **OpenAI-Compatible** with \`http://127.0.0.1:8081/v1\`, or add the model id manually under MLX if the endpoint exposes multiple ids.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export const AppleFoundationModelsSetupInstructions = () => {
|
||||
if (os !== 'mac') return null
|
||||
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 text-void-fg-3 text-sm list-decimal select-text mb-4'>
|
||||
<div><ChatMarkdownRender string={`Apple (one model: \`foundation\`)`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`1. Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled. Void can install [\`afm\`](https://github.com/scouzi1966/maclocal-api) via Homebrew (toggle in Settings → Models).`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`2. Only one **autodetected** entry (\`foundation\`): the on-device model exposed by \`afm\`.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`3. **Fine-tuned adapter**: \`afm -a ./my-adapter.fmadapter -p 9998\`, then set Void’s endpoint to \`http://127.0.0.1:9998\` and refresh.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`4. **Other models (Llama, Qwen, etc.)**: use **MLX** or **Ollama** — Apple FM only serves Apple’s built-in Foundation model.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const RedoOnboardingButton = ({ className }: { className?: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
|
@ -1181,6 +1254,8 @@ export const Settings = () => {
|
|||
<ModelDump />
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
<AutoDetectLocalModelsToggle />
|
||||
<AutoSetupMlxToggle />
|
||||
<AutoSetupAppleFoundationModelsToggle />
|
||||
<RefreshableModels />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
|
@ -1194,6 +1269,10 @@ export const Settings = () => {
|
|||
<div className='opacity-80 mb-4'>
|
||||
<OllamaSetupInstructions sayWeAutoDetect={true} />
|
||||
</div>
|
||||
<div className='opacity-80'>
|
||||
<MlxSetupInstructions />
|
||||
<AppleFoundationModelsSetupInstructions />
|
||||
</div>
|
||||
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ import '../common/voidSettingsService.js'
|
|||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// Apple Foundation Models (macOS): auto-install afm + start server
|
||||
import '../common/appleFoundationModelsService.js'
|
||||
import './appleFoundationModelsWorkbenchContrib.js'
|
||||
|
||||
// MLX (macOS): auto-install mlx-lm + start mlx_lm.server
|
||||
import '../common/mlxService.js'
|
||||
import './mlxWorkbenchContrib.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { AppleFoundationModelsEnsureResult, IAppleFoundationModelsMainService } from './appleFoundationModelsTypes.js';
|
||||
|
||||
export interface IAppleFoundationModelsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number }): Promise<AppleFoundationModelsEnsureResult>;
|
||||
}
|
||||
|
||||
export const IAppleFoundationModelsService = createDecorator<IAppleFoundationModelsService>('appleFoundationModelsService');
|
||||
|
||||
export class AppleFoundationModelsService implements IAppleFoundationModelsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly _main: IAppleFoundationModelsMainService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
) {
|
||||
this._main = ProxyChannel.toService<IAppleFoundationModelsMainService>(mainProcessService.getChannel('void-channel-appleFoundationModels'));
|
||||
}
|
||||
|
||||
ensureReady = (options: { installIfMissing: boolean; startServer: boolean; port?: number }): Promise<AppleFoundationModelsEnsureResult> => {
|
||||
return this._main.ensureReady(options);
|
||||
};
|
||||
}
|
||||
|
||||
registerSingleton(IAppleFoundationModelsService, AppleFoundationModelsService, InstantiationType.Eager);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
export const APPLE_FOUNDATION_MODELS_DEFAULT_ENDPOINT = 'http://127.0.0.1:9999';
|
||||
export const APPLE_FOUNDATION_MODELS_DEFAULT_PORT = 9999;
|
||||
|
||||
export type AppleFoundationModelsEnsureAction =
|
||||
| 'already-running'
|
||||
| 'started'
|
||||
| 'installed-and-started';
|
||||
|
||||
export type AppleFoundationModelsEnsureFailureReason =
|
||||
| 'not-mac'
|
||||
| 'disabled'
|
||||
| 'brew-missing'
|
||||
| 'install-failed'
|
||||
| 'afm-missing'
|
||||
| 'server-timeout';
|
||||
|
||||
export type AppleFoundationModelsEnsureResult =
|
||||
| { ok: true; endpoint: string; action: AppleFoundationModelsEnsureAction; log: string[] }
|
||||
| { ok: false; reason: AppleFoundationModelsEnsureFailureReason; log: string[]; errorMessage?: string };
|
||||
|
||||
export interface IAppleFoundationModelsMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number }): Promise<AppleFoundationModelsEnsureResult>;
|
||||
stopServerIfSpawnedByVoid(): Promise<void>;
|
||||
}
|
||||
|
||||
export const IAppleFoundationModelsMainService = createDecorator<IAppleFoundationModelsMainService>('appleFoundationModelsMainService');
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProviderName, VoidStatefulModelInfo } from './voidSettingsTypes.js';
|
||||
|
||||
/** One loaded model per endpoint (mlx_lm.server / afm). */
|
||||
export const singleAutodetectedLocalProviders = ['mlx', 'appleFoundationModels'] as const satisfies ProviderName[]
|
||||
|
||||
export type SingleAutodetectedLocalProvider = typeof singleAutodetectedLocalProviders[number]
|
||||
|
||||
const canonicalAppleFoundationModelName = (modelName: string) =>
|
||||
modelName === 'foundation-models' || modelName === 'foundation-model' ? 'foundation' : modelName
|
||||
|
||||
export const normalizeAutodetectedModelNamesForProvider = (providerName: ProviderName, modelNames: string[]): string[] => {
|
||||
if (providerName === 'appleFoundationModels') {
|
||||
const normalized = modelNames.map(canonicalAppleFoundationModelName)
|
||||
return [new Set(normalized).values().next().value ?? 'foundation']
|
||||
}
|
||||
if (providerName === 'mlx') {
|
||||
const unique = [...new Set(modelNames)]
|
||||
return unique.length > 0 ? [unique[0]] : []
|
||||
}
|
||||
return modelNames
|
||||
}
|
||||
|
||||
export const consolidateSingleAutodetectedProviderModels = (
|
||||
providerName: SingleAutodetectedLocalProvider,
|
||||
mergedModels: VoidStatefulModelInfo[],
|
||||
): VoidStatefulModelInfo[] => {
|
||||
const customModels = mergedModels.filter(m => m.type === 'custom')
|
||||
const autodetected = mergedModels.find(m => m.type === 'autodetected')
|
||||
if (!autodetected) {
|
||||
if (providerName === 'appleFoundationModels') {
|
||||
return [{ modelName: 'foundation', type: 'autodetected', isHidden: false }]
|
||||
}
|
||||
return customModels
|
||||
}
|
||||
const primaryName = providerName === 'appleFoundationModels'
|
||||
? canonicalAppleFoundationModelName(autodetected.modelName)
|
||||
: autodetected.modelName
|
||||
const primary: VoidStatefulModelInfo = {
|
||||
...autodetected,
|
||||
modelName: primaryName,
|
||||
type: 'autodetected',
|
||||
}
|
||||
return [
|
||||
primary,
|
||||
...customModels.filter(m => m.modelName !== primaryName),
|
||||
]
|
||||
}
|
||||
34
src/vs/workbench/contrib/void/common/mlxService.ts
Normal file
34
src/vs/workbench/contrib/void/common/mlxService.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { IMlxMainService, MlxEnsureResult } from './mlxTypes.js';
|
||||
|
||||
export interface IMlxService {
|
||||
readonly _serviceBrand: undefined;
|
||||
ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number; model?: string }): Promise<MlxEnsureResult>;
|
||||
}
|
||||
|
||||
export const IMlxService = createDecorator<IMlxService>('mlxService');
|
||||
|
||||
export class MlxService implements IMlxService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly _main: IMlxMainService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
) {
|
||||
this._main = ProxyChannel.toService<IMlxMainService>(mainProcessService.getChannel('void-channel-mlx'));
|
||||
}
|
||||
|
||||
ensureReady = (options: { installIfMissing: boolean; startServer: boolean; port?: number; model?: string }): Promise<MlxEnsureResult> => {
|
||||
return this._main.ensureReady(options);
|
||||
};
|
||||
}
|
||||
|
||||
registerSingleton(IMlxService, MlxService, InstantiationType.Eager);
|
||||
32
src/vs/workbench/contrib/void/common/mlxTypes.ts
Normal file
32
src/vs/workbench/contrib/void/common/mlxTypes.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
export const MLX_DEFAULT_ENDPOINT = 'http://127.0.0.1:8080';
|
||||
export const MLX_DEFAULT_PORT = 8080;
|
||||
/** Small default model; first start may download weights from Hugging Face. */
|
||||
export const MLX_DEFAULT_MODEL = 'mlx-community/Qwen2.5-Coder-1.5B-Instruct';
|
||||
|
||||
export type MlxEnsureAction = 'already-running' | 'started' | 'installed-and-started';
|
||||
|
||||
export type MlxEnsureFailureReason =
|
||||
| 'not-mac'
|
||||
| 'python-missing'
|
||||
| 'install-failed'
|
||||
| 'mlx-missing'
|
||||
| 'server-timeout';
|
||||
|
||||
export type MlxEnsureResult =
|
||||
| { ok: true; endpoint: string; action: MlxEnsureAction; log: string[] }
|
||||
| { ok: false; reason: MlxEnsureFailureReason; log: string[]; errorMessage?: string };
|
||||
|
||||
export interface IMlxMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number; model?: string }): Promise<MlxEnsureResult>;
|
||||
stopServerIfSpawnedByVoid(): Promise<void>;
|
||||
}
|
||||
|
||||
export const IMlxMainService = createDecorator<IMlxMainService>('mlxMainService');
|
||||
|
|
@ -48,6 +48,12 @@ export const defaultProviderSettings = {
|
|||
lmStudio: {
|
||||
endpoint: 'http://localhost:1234',
|
||||
},
|
||||
mlx: {
|
||||
endpoint: 'http://127.0.0.1:8080',
|
||||
},
|
||||
appleFoundationModels: {
|
||||
endpoint: 'http://127.0.0.1:9999',
|
||||
},
|
||||
liteLLM: { // https://docs.litellm.ai/docs/providers/openai_compatible
|
||||
endpoint: '',
|
||||
},
|
||||
|
|
@ -111,7 +117,8 @@ export const defaultModelsOfProvider = {
|
|||
vLLM: [ // autodetected
|
||||
],
|
||||
lmStudio: [], // autodetected
|
||||
|
||||
mlx: [], // autodetected — mlx_lm.server
|
||||
appleFoundationModels: [], // autodetected via afm /v1/models (model id: `foundation`)
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
'anthropic/claude-opus-4.7',
|
||||
'anthropic/claude-sonnet-4.6',
|
||||
|
|
@ -1594,6 +1601,47 @@ const lmStudioSettings: VoidStaticProviderInfo = {
|
|||
},
|
||||
}
|
||||
|
||||
const mlxSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
}
|
||||
|
||||
const appleFoundationModelCapabilities = {
|
||||
contextWindow: 32_768,
|
||||
reservedOutputTokenSpace: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false as const,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role' as const,
|
||||
specialToolFormat: 'openai-style' as const,
|
||||
reasoningCapabilities: false as const,
|
||||
}
|
||||
|
||||
const appleFoundationModelsModelOptions = {
|
||||
'foundation': { // Apple on-device model via afm — https://github.com/scouzi1966/maclocal-api
|
||||
...appleFoundationModelCapabilities,
|
||||
},
|
||||
} as const satisfies { [s: string]: VoidStaticModelInfo }
|
||||
|
||||
const appleFoundationModelsSettings: VoidStaticProviderInfo = {
|
||||
modelOptions: appleFoundationModelsModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
if (lower.includes('foundation') || lower.includes('apple') || lower.includes('afm')) {
|
||||
return { modelName, recognizedModelName: 'foundation', ...appleFoundationModelsModelOptions['foundation'] }
|
||||
}
|
||||
return extensiveModelOptionsFallback(modelName, { downloadable: false, contextWindow: 32_768 })
|
||||
},
|
||||
providerReasoningIOSettings: {
|
||||
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
}
|
||||
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: ollamaModelOptions,
|
||||
|
|
@ -1867,6 +1915,8 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
|
||||
liteLLM: liteLLMSettings,
|
||||
lmStudio: lmStudioSettings,
|
||||
mlx: mlxSettings,
|
||||
appleFoundationModels: appleFoundationModelsSettings,
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
|
|||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { normalizeAutodetectedModelNamesForProvider } from './localSingleModelProviders.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
|
@ -47,6 +48,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
|
|||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
|
||||
mlx: ['_didFillInProviderSettings', 'endpoint'],
|
||||
appleFoundationModels: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
|
|
@ -144,6 +147,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
ollama: { state: 'init', timeoutId: null },
|
||||
vLLM: { state: 'init', timeoutId: null },
|
||||
lmStudio: { state: 'init', timeoutId: null },
|
||||
mlx: { state: 'init', timeoutId: null },
|
||||
appleFoundationModels: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -168,14 +173,17 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
providerName,
|
||||
onSuccess: ({ models }) => {
|
||||
// set the models to the detected models
|
||||
const modelNames = models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'vLLM' || providerName === 'lmStudio' || providerName === 'mlx' || providerName === 'appleFoundationModels') {
|
||||
return (model as OpenaiCompatibleModelResponse).id;
|
||||
}
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
});
|
||||
const normalizedModelNames = normalizeAutodetectedModelNamesForProvider(providerName, modelNames);
|
||||
this.voidSettingsService.setAutodetectedModels(
|
||||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'lmStudio') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
normalizedModelNames,
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
|
|||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { consolidateSingleAutodetectedProviderModels, normalizeAutodetectedModelNamesForProvider } from './localSingleModelProviders.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
|
|
@ -103,7 +105,6 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo
|
|||
]
|
||||
}
|
||||
|
||||
|
||||
export const modelFilterOfFeatureName: {
|
||||
[featureName in FeatureName]: {
|
||||
filter: (
|
||||
|
|
@ -292,6 +293,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
// add autoAcceptLLMChanges feature
|
||||
if (readS.globalSettings.autoAcceptLLMChanges === undefined) readS.globalSettings.autoAcceptLLMChanges = false;
|
||||
|
||||
// Apple Foundation Models auto-setup (macOS only; default off on other platforms)
|
||||
if (readS.globalSettings.autoSetupAppleFoundationModels === undefined) {
|
||||
readS.globalSettings.autoSetupAppleFoundationModels = isMacintosh;
|
||||
}
|
||||
if (readS.globalSettings.autoSetupMlx === undefined) {
|
||||
readS.globalSettings.autoSetupMlx = isMacintosh;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
readS = defaultState()
|
||||
|
|
@ -329,6 +338,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
if (providerName === 'openAICompatible' && !readS.settingsOfProvider[providerName].headersJSON) {
|
||||
readS.settingsOfProvider[providerName].headersJSON = '{}'
|
||||
}
|
||||
|
||||
if (providerName === 'mlx' || providerName === 'appleFoundationModels') {
|
||||
readS.settingsOfProvider[providerName].models = consolidateSingleAutodetectedProviderModels(
|
||||
providerName,
|
||||
readS.settingsOfProvider[providerName].models,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +520,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const oldModelNames = models.map(m => m.modelName)
|
||||
|
||||
const newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: autodetectedModelNames, type: 'autodetected' })
|
||||
const normalizedNames = normalizeAutodetectedModelNamesForProvider(providerName, autodetectedModelNames)
|
||||
let newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: normalizedNames, type: 'autodetected' })
|
||||
if (providerName === 'mlx' || providerName === 'appleFoundationModels') {
|
||||
newModels = consolidateSingleAutodetectedProviderModels(providerName, newModels)
|
||||
}
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ 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'] satisfies ProviderName[] // all local names
|
||||
export const localProviderNames = ['ollama', 'vLLM', 'lmStudio', 'mlx', 'appleFoundationModels'] 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]>
|
||||
|
|
@ -82,6 +82,12 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
else if (providerName === 'lmStudio') {
|
||||
return { title: 'LM Studio', }
|
||||
}
|
||||
else if (providerName === 'mlx') {
|
||||
return { title: 'MLX', }
|
||||
}
|
||||
else if (providerName === 'appleFoundationModels') {
|
||||
return { title: 'Apple', }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return { title: 'OpenAI-Compatible', }
|
||||
}
|
||||
|
|
@ -127,6 +133,8 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
|||
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
|
||||
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
|
||||
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||
if (providerName === 'mlx') return 'Only one loaded model is listed at a time (autodetected). See the MLX instructions below to switch models or add a second one.'
|
||||
if (providerName === 'appleFoundationModels') return 'Only one on-device model (`foundation`, autodetected). Void can install and run [`afm`](https://github.com/scouzi1966/maclocal-api) automatically. See the instructions below for adapters or other models.'
|
||||
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
|
|
@ -166,6 +174,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'mlx' ? 'Endpoint' :
|
||||
providerName === 'appleFoundationModels' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
|
|
@ -177,6 +187,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'mlx' ? defaultProviderSettings.mlx.endpoint
|
||||
: providerName === 'appleFoundationModels' ? defaultProviderSettings.appleFoundationModels.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
|
|
@ -334,6 +346,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
mlx: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.mlx,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mlx),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
appleFoundationModels: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.appleFoundationModels,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.appleFoundationModels),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
googleVertex: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.googleVertex,
|
||||
|
|
@ -440,6 +464,10 @@ export type ChatMode = 'agent' | 'gather' | 'normal'
|
|||
|
||||
export type GlobalSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
/** macOS only: install `afm` via Homebrew and start the local Apple Foundation Models server */
|
||||
autoSetupAppleFoundationModels: boolean;
|
||||
/** macOS only: install `mlx-lm` via pip and start mlx_lm.server */
|
||||
autoSetupMlx: boolean;
|
||||
aiInstructions: string;
|
||||
enableAutocomplete: boolean;
|
||||
syncApplyToChat: boolean;
|
||||
|
|
@ -456,6 +484,8 @@ export type GlobalSettings = {
|
|||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
autoRefreshModels: true,
|
||||
autoSetupAppleFoundationModels: true,
|
||||
autoSetupMlx: true,
|
||||
aiInstructions: '',
|
||||
enableAutocomplete: false,
|
||||
syncApplyToChat: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { exec as _exec } from 'child_process';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import {
|
||||
APPLE_FOUNDATION_MODELS_DEFAULT_PORT,
|
||||
AppleFoundationModelsEnsureResult,
|
||||
IAppleFoundationModelsMainService,
|
||||
} from '../common/appleFoundationModelsTypes.js';
|
||||
|
||||
const exec = promisify(_exec);
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export class AppleFoundationModelsMainService implements IAppleFoundationModelsMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private _child: ChildProcess | null = null;
|
||||
private _spawnedByVoid = false;
|
||||
|
||||
async ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number }): Promise<AppleFoundationModelsEnsureResult> {
|
||||
const log: string[] = [];
|
||||
const port = options.port ?? APPLE_FOUNDATION_MODELS_DEFAULT_PORT;
|
||||
const endpoint = `http://127.0.0.1:${port}`;
|
||||
|
||||
if (!isMacintosh) {
|
||||
return { ok: false, reason: 'not-mac', log };
|
||||
}
|
||||
|
||||
if (await this._isServerUp(endpoint)) {
|
||||
log.push(`Serveur déjà actif sur ${endpoint}`);
|
||||
return { ok: true, endpoint, action: 'already-running', log };
|
||||
}
|
||||
|
||||
let didInstall = false;
|
||||
let afmPath = await this._whichAfm();
|
||||
if (!afmPath) {
|
||||
if (!options.installIfMissing) {
|
||||
log.push('Commande `afm` introuvable.');
|
||||
return { ok: false, reason: 'afm-missing', log, errorMessage: 'Installez afm avec Homebrew : brew tap scouzi1966/afm && brew install afm' };
|
||||
}
|
||||
|
||||
const brewPath = await this._whichBrew();
|
||||
if (!brewPath) {
|
||||
log.push('Homebrew introuvable — impossible d’installer afm automatiquement.');
|
||||
return { ok: false, reason: 'brew-missing', log, errorMessage: 'Installez Homebrew puis : brew tap scouzi1966/afm && brew install afm' };
|
||||
}
|
||||
|
||||
log.push('Installation de afm via Homebrew…');
|
||||
try {
|
||||
await exec(`${brewPath} tap scouzi1966/afm`, { timeout: 120_000 });
|
||||
await exec(`${brewPath} install afm`, { timeout: 600_000 });
|
||||
didInstall = true;
|
||||
log.push('Installation Homebrew terminée.');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
log.push(`Échec installation : ${msg}`);
|
||||
return { ok: false, reason: 'install-failed', log, errorMessage: msg };
|
||||
}
|
||||
|
||||
afmPath = await this._whichAfm();
|
||||
if (!afmPath) {
|
||||
log.push('afm toujours introuvable après installation.');
|
||||
return { ok: false, reason: 'afm-missing', log };
|
||||
}
|
||||
} else {
|
||||
log.push(`afm trouvé : ${afmPath}`);
|
||||
}
|
||||
|
||||
if (options.startServer) {
|
||||
await this._startServer(afmPath, port, log);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 45; i++) {
|
||||
if (await this._isServerUp(endpoint)) {
|
||||
const action = didInstall ? 'installed-and-started' as const : 'started' as const;
|
||||
log.push(`Serveur prêt sur ${endpoint}`);
|
||||
return { ok: true, endpoint, action, log };
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
log.push('Délai dépassé en attendant le serveur afm.');
|
||||
return { ok: false, reason: 'server-timeout', log, errorMessage: `Le serveur n’a pas répondu sur ${endpoint}. Lancez \`afm -p ${port}\` manuellement.` };
|
||||
}
|
||||
|
||||
async stopServerIfSpawnedByVoid(): Promise<void> {
|
||||
if (!this._spawnedByVoid || !this._child || this._child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._child.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this._child = null;
|
||||
this._spawnedByVoid = false;
|
||||
}
|
||||
|
||||
private async _whichAfm(): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await exec('which afm', { timeout: 5_000 });
|
||||
const path = stdout.trim();
|
||||
return path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _whichBrew(): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await exec('which brew', { timeout: 5_000 });
|
||||
const path = stdout.trim();
|
||||
return path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _isServerUp(endpoint: string): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2_500);
|
||||
try {
|
||||
const health = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
||||
if (health.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// try models next
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
const controller2 = new AbortController();
|
||||
const timeout2 = setTimeout(() => controller2.abort(), 2_500);
|
||||
try {
|
||||
const models = await fetch(`${endpoint}/v1/models`, { signal: controller2.signal });
|
||||
return models.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout2);
|
||||
}
|
||||
}
|
||||
|
||||
private async _startServer(afmPath: string, port: number, log: string[]): Promise<void> {
|
||||
if (this._child && !this._child.killed) {
|
||||
log.push('Processus afm Void déjà en cours.');
|
||||
return;
|
||||
}
|
||||
|
||||
log.push(`Démarrage de afm sur le port ${port}…`);
|
||||
const child = spawn(afmPath, ['-p', String(port), '-H', '127.0.0.1'], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
child.unref();
|
||||
this._child = child;
|
||||
this._spawnedByVoid = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -139,6 +139,14 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName }: { se
|
|||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'mlx') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'appleFoundationModels') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
|
|
@ -963,6 +971,16 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
mlx: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
appleFoundationModels: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
liteLLM: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
|
|
|
|||
174
src/vs/workbench/contrib/void/electron-main/mlxMainService.ts
Normal file
174
src/vs/workbench/contrib/void/electron-main/mlxMainService.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { exec as _exec } from 'child_process';
|
||||
import { isMacintosh } from '../../../../base/common/platform.js';
|
||||
import {
|
||||
MLX_DEFAULT_MODEL,
|
||||
MLX_DEFAULT_PORT,
|
||||
IMlxMainService,
|
||||
MlxEnsureResult,
|
||||
} from '../common/mlxTypes.js';
|
||||
|
||||
const exec = promisify(_exec);
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
|
||||
type MlxServerCommand = { command: string; baseArgs: string[] };
|
||||
|
||||
export class MlxMainService implements IMlxMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private _child: ChildProcess | null = null;
|
||||
private _spawnedByVoid = false;
|
||||
|
||||
async ensureReady(options: { installIfMissing: boolean; startServer: boolean; port?: number; model?: string }): Promise<MlxEnsureResult> {
|
||||
const log: string[] = [];
|
||||
const port = options.port ?? MLX_DEFAULT_PORT;
|
||||
const model = options.model ?? MLX_DEFAULT_MODEL;
|
||||
const endpoint = `http://127.0.0.1:${port}`;
|
||||
|
||||
if (!isMacintosh) {
|
||||
return { ok: false, reason: 'not-mac', log };
|
||||
}
|
||||
|
||||
if (await this._isServerUp(endpoint)) {
|
||||
log.push(`Server already running at ${endpoint}`);
|
||||
return { ok: true, endpoint, action: 'already-running', log };
|
||||
}
|
||||
|
||||
const pythonPath = await this._whichPython3();
|
||||
if (!pythonPath) {
|
||||
log.push('python3 not found.');
|
||||
return { ok: false, reason: 'python-missing', log, errorMessage: 'Install Python 3, then run: python3 -m pip install mlx-lm' };
|
||||
}
|
||||
log.push(`Using ${pythonPath}`);
|
||||
|
||||
let didInstall = false;
|
||||
let serverCmd = await this._resolveMlxServerCommand(pythonPath);
|
||||
if (!serverCmd) {
|
||||
if (!options.installIfMissing) {
|
||||
log.push('mlx-lm is not installed.');
|
||||
return { ok: false, reason: 'mlx-missing', log, errorMessage: 'Run: python3 -m pip install mlx-lm' };
|
||||
}
|
||||
|
||||
log.push('Installing mlx-lm via pip…');
|
||||
try {
|
||||
await exec(`"${pythonPath}" -m pip install --upgrade mlx-lm`, { timeout: 600_000 });
|
||||
didInstall = true;
|
||||
log.push('pip install finished.');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
log.push(`Install failed: ${msg}`);
|
||||
return { ok: false, reason: 'install-failed', log, errorMessage: msg };
|
||||
}
|
||||
|
||||
serverCmd = await this._resolveMlxServerCommand(pythonPath);
|
||||
if (!serverCmd) {
|
||||
log.push('mlx-lm still not available after install.');
|
||||
return { ok: false, reason: 'mlx-missing', log };
|
||||
}
|
||||
}
|
||||
|
||||
if (options.startServer) {
|
||||
await this._startServer(serverCmd, port, model, log);
|
||||
}
|
||||
|
||||
// First launch may download the model from Hugging Face (can take several minutes).
|
||||
for (let i = 0; i < 300; i++) {
|
||||
if (await this._isServerUp(endpoint)) {
|
||||
const action = didInstall ? 'installed-and-started' as const : 'started' as const;
|
||||
log.push(`Server ready at ${endpoint}`);
|
||||
return { ok: true, endpoint, action, log };
|
||||
}
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
log.push('Timed out waiting for mlx_lm.server.');
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'server-timeout',
|
||||
log,
|
||||
errorMessage: `Server did not respond at ${endpoint}. Run manually: mlx_lm.server --model ${model} --port ${port}`,
|
||||
};
|
||||
}
|
||||
|
||||
async stopServerIfSpawnedByVoid(): Promise<void> {
|
||||
if (!this._spawnedByVoid || !this._child || this._child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._child.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this._child = null;
|
||||
this._spawnedByVoid = false;
|
||||
}
|
||||
|
||||
private async _whichPython3(): Promise<string | null> {
|
||||
for (const cmd of ['python3', 'python']) {
|
||||
try {
|
||||
const { stdout } = await exec(`which ${cmd}`, { timeout: 5_000 });
|
||||
const path = stdout.trim();
|
||||
if (path) return path;
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async _resolveMlxServerCommand(pythonPath: string): Promise<MlxServerCommand | null> {
|
||||
try {
|
||||
const { stdout } = await exec('which mlx_lm.server', { timeout: 5_000 });
|
||||
const path = stdout.trim();
|
||||
if (path) {
|
||||
return { command: path, baseArgs: [] };
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
try {
|
||||
await exec(`"${pythonPath}" -m mlx_lm.server --help`, { timeout: 10_000 });
|
||||
return { command: pythonPath, baseArgs: ['-m', 'mlx_lm.server'] };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _isServerUp(endpoint: string): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2_500);
|
||||
try {
|
||||
const res = await fetch(`${endpoint}/v1/models`, { signal: controller.signal });
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async _startServer(serverCmd: MlxServerCommand, port: number, model: string, log: string[]): Promise<void> {
|
||||
if (this._child && !this._child.killed) {
|
||||
log.push('Void mlx server process already running.');
|
||||
return;
|
||||
}
|
||||
|
||||
log.push(`Starting mlx_lm.server on port ${port} with model ${model}…`);
|
||||
const args = [...serverCmd.baseArgs, '--model', model, '--port', String(port)];
|
||||
const child = spawn(serverCmd.command, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
child.unref();
|
||||
this._child = child;
|
||||
this._spawnedByVoid = true;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue