mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
Merge branch 'AppleModels_And_MistralFetchModels' into dev
This commit is contained in:
commit
657dc425a3
8 changed files with 213 additions and 19 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.99.3",
|
||||
"version": "2.0.0 - Apple Foundation Model + Mistral Fetch support",
|
||||
"distro": "21c8d8ea1e46d97c5639a7cabda6c0e063cc8dd5",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.4.9",
|
||||
"voidVersion": "2.0.0 - Apple Foundation Model + Mistral Fetch support",
|
||||
"voidRelease": "0044",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/se
|
|||
import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
|
||||
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
|
||||
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
|
||||
import { startAfmIfNeeded } from '../../workbench/contrib/void/electron-main/afmBackendService.js';
|
||||
/**
|
||||
* The main VS Code application. There will only ever be one instance,
|
||||
* even if the user starts many instances (e.g. from the command line).
|
||||
|
|
@ -1389,6 +1390,9 @@ export class CodeApplication extends Disposable {
|
|||
|
||||
private afterWindowOpen(): void {
|
||||
|
||||
// Void: start afm (Apple Foundation Model) backend if available
|
||||
this.startAfmBackend();
|
||||
|
||||
// Windows: mutex
|
||||
this.installMutex();
|
||||
|
||||
|
|
@ -1415,6 +1419,13 @@ export class CodeApplication extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
private startAfmBackend(): void {
|
||||
startAfmIfNeeded(
|
||||
(cb) => Event.once(this.lifecycleMainService.onWillShutdown)(() => cb()),
|
||||
(msg) => this.logService.info(msg),
|
||||
).catch((err) => this.logService.error(`[Void] afm: unexpected error: ${err}`));
|
||||
}
|
||||
|
||||
private async installMutex(): Promise<void> {
|
||||
const win32MutexName = this.productService.win32MutexName;
|
||||
if (isWindows && win32MutexName) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ export const defaultProviderSettings = {
|
|||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
macLocalAFM: {
|
||||
endpoint: 'http://localhost:9999',
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
|
@ -152,6 +155,7 @@ export const defaultModelsOfProvider = {
|
|||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
macLocalAFM: [], // autodetected via afm list endpoint
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
|
@ -1459,6 +1463,22 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
|||
|
||||
|
||||
|
||||
// ---------------- MAC LOCAL AFM (Apple Foundation Model / MLX via afm) ----------------
|
||||
const macLocalAFMSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: false }),
|
||||
modelOptions: {
|
||||
'foundation': {
|
||||
contextWindow: 4_096,
|
||||
reservedOutputTokenSpace: 2_048,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
|
||||
|
|
@ -1484,6 +1504,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
macLocalAFM: macLocalAFMSettings,
|
||||
} as const
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { IVoidSettingsService } from './voidSettingsService.js';
|
|||
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 { localProviderNames, RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.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 +47,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
|
|||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
|
||||
macLocalAFM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
mistral: ['_didFillInProviderSettings', 'apiKey'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
|
|
@ -144,6 +146,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
ollama: { state: 'init', timeoutId: null },
|
||||
vLLM: { state: 'init', timeoutId: null },
|
||||
lmStudio: { state: 'init', timeoutId: null },
|
||||
macLocalAFM: { state: 'init', timeoutId: null },
|
||||
mistral: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -154,8 +158,10 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
this._setRefreshState(providerName, 'refreshing', options)
|
||||
|
||||
const isLocalProvider = (localProviderNames as string[]).includes(providerName)
|
||||
const autoPoll = () => {
|
||||
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// only continuously poll local providers (cloud providers like Mistral don't need it)
|
||||
if (isLocalProvider && this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// resume auto-polling
|
||||
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
|
|
@ -171,10 +177,12 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
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);
|
||||
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 if (providerName === 'macLocalAFM') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'mistral') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ 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', 'macLocalAFM'] satisfies ProviderName[] // all local names (no API key, endpoint-based)
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
// providers whose model list can be fetched and auto-refreshed (local + cloud providers with a /v1/models endpoint)
|
||||
export const refreshableProviderNames = ['ollama', 'vLLM', 'lmStudio', 'macLocalAFM', 'mistral'] satisfies ProviderName[]
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
|
||||
|
|
@ -106,6 +109,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
else if (providerName === 'awsBedrock') {
|
||||
return { title: 'AWS Bedrock', }
|
||||
}
|
||||
else if (providerName === 'macLocalAFM') {
|
||||
return { title: 'Apple Foundation Model', }
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
|
@ -128,6 +134,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
|||
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 === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||
if (providerName === 'macLocalAFM') return 'Run Apple\'s on-device Foundation Model or any MLX model locally. Install [afm](https://github.com/scouzi1966/maclocal-api) and start with `afm` (Foundation Model) or `afm mlx -m <model>` (MLX). Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled.'
|
||||
|
||||
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
|
@ -163,9 +170,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'macLocalAFM' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
|
|
@ -173,10 +181,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'awsBedrock' ? 'Endpoint' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||
: providerName === 'macLocalAFM' ? defaultProviderSettings.macLocalAFM.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
|
|
@ -352,6 +361,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
macLocalAFM: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.macLocalAFM,
|
||||
...modelInfoOfDefaultModelNames([...defaultModelsOfProvider.macLocalAFM]),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -385,8 +400,6 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
|||
}
|
||||
|
||||
|
||||
// the models of these can be refreshed (in theory all can, but not all should)
|
||||
export const refreshableProviderNames = localProviderNames
|
||||
export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
// models that come with download buttons
|
||||
|
|
|
|||
131
src/vs/workbench/contrib/void/electron-main/afmBackendService.ts
Normal file
131
src/vs/workbench/contrib/void/electron-main/afmBackendService.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { spawn, exec as execCb, ChildProcess } from 'child_process';
|
||||
import { createServer } from 'net';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(execCb);
|
||||
const AFM_DEFAULT_PORT = 9999;
|
||||
|
||||
/**
|
||||
* Returns true if something is already listening on the given port.
|
||||
*/
|
||||
const isPortInUse = (port: number): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const server = createServer();
|
||||
server.once('error', () => resolve(true)); // port is taken
|
||||
server.once('listening', () => {
|
||||
server.close(() => resolve(false)); // port is free
|
||||
});
|
||||
server.listen(port, '127.0.0.1');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given command is available in PATH.
|
||||
*/
|
||||
const isCommandAvailable = async (cmd: string): Promise<boolean> => {
|
||||
try {
|
||||
await exec(`which ${cmd}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to install `afm` via Homebrew.
|
||||
* Returns true if installation succeeded.
|
||||
*/
|
||||
const installAfmViaBrew = async (log: (msg: string) => void): Promise<boolean> => {
|
||||
const brewAvailable = await isCommandAvailable('brew');
|
||||
if (!brewAvailable) {
|
||||
log('[Void] afm: not found and Homebrew is not installed. Install afm manually: brew install scouzi1966/afm/afm');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('[Void] afm: not found — installing via Homebrew (this may take a moment)…');
|
||||
try {
|
||||
await exec('brew install scouzi1966/afm/afm');
|
||||
log('[Void] afm: installed successfully via Homebrew');
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`[Void] afm: Homebrew installation failed: ${e}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to start the `afm -g` backend (Apple Foundation Model + API gateway).
|
||||
* - Only runs on macOS.
|
||||
* - If `afm` is not installed, auto-installs it via Homebrew.
|
||||
* - If port 9999 is already in use (user started afm manually), leaves it untouched.
|
||||
* - If we spawn the process, we register a shutdown hook to kill it cleanly when
|
||||
* Void exits.
|
||||
*/
|
||||
export const startAfmIfNeeded = async (
|
||||
onShutdown: (cb: () => void) => void,
|
||||
log: (msg: string) => void,
|
||||
): Promise<void> => {
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
return; // afm is macOS-only
|
||||
}
|
||||
|
||||
const portInUse = await isPortInUse(AFM_DEFAULT_PORT);
|
||||
if (portInUse) {
|
||||
log('[Void] afm: port 9999 already in use — using existing afm instance');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-install if afm is not in PATH
|
||||
const afmAvailable = await isCommandAvailable('afm');
|
||||
if (!afmAvailable) {
|
||||
const installed = await installAfmViaBrew(log);
|
||||
if (!installed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn afm -g (gateway mode: auto-discovers Ollama, LM Studio, Jan, etc.)
|
||||
let afmProcess: ChildProcess | null = null;
|
||||
|
||||
try {
|
||||
afmProcess = spawn('afm', ['-g'], {
|
||||
detached: false,
|
||||
stdio: 'ignore',
|
||||
env: { ...process.env },
|
||||
});
|
||||
} catch (e) {
|
||||
log(`[Void] afm: could not start: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
afmProcess.on('error', (err) => {
|
||||
log(`[Void] afm: error: ${err.message}`);
|
||||
afmProcess = null;
|
||||
});
|
||||
|
||||
afmProcess.on('exit', (code, signal) => {
|
||||
log(`[Void] afm: exited (code=${code}, signal=${signal})`);
|
||||
afmProcess = null;
|
||||
});
|
||||
|
||||
log('[Void] afm: started on port 9999 with gateway mode (-g)');
|
||||
|
||||
// Kill afm when Void shuts down — only if WE started it
|
||||
onShutdown(() => {
|
||||
if (afmProcess) {
|
||||
log('[Void] afm: stopping on Void shutdown');
|
||||
try {
|
||||
afmProcess.kill();
|
||||
} catch (_) {
|
||||
// process may have already exited
|
||||
}
|
||||
afmProcess = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -167,6 +167,11 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
|
|||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'macLocalAFM') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
const endpoint = thisConfig.endpoint || 'http://localhost:9999'
|
||||
return new OpenAI({ baseURL: `${endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
else throw new Error(`Void providerName was invalid: ${providerName}.`)
|
||||
}
|
||||
|
|
@ -878,7 +883,7 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
mistral: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => sendMistralFIM(params),
|
||||
list: null,
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
ollama: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
|
|
@ -937,6 +942,11 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
macLocalAFM: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue