Merge branch 'AppleModels_And_MistralFetchModels' into dev

This commit is contained in:
Jérôme Commaret 2026-04-23 23:38:04 +02:00
commit 657dc425a3
8 changed files with 213 additions and 19 deletions

View file

@ -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"

View file

@ -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",

View file

@ -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) {

View file

@ -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

View file

@ -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 }
)

View file

@ -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

View 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;
}
});
};

View file

@ -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