From 5750eb007d06be7c3e0e748f7c7417253537516b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 23 Apr 2026 23:33:28 +0200 Subject: [PATCH 1/9] Added Apple Foundation model And Mistral fetch model to the list of models. --- src/vs/code/electron-main/app.ts | 11 ++ .../contrib/void/common/modelCapabilities.ts | 21 +++ .../void/common/refreshModelService.ts | 20 ++- .../contrib/void/common/voidSettingsTypes.ts | 33 +++-- .../void/electron-main/afmBackendService.ts | 131 ++++++++++++++++++ .../llmMessage/sendLLMMessage.impl.ts | 12 +- 6 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 src/vs/workbench/contrib/void/electron-main/afmBackendService.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c3d2dfe5..755b259f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -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 { const win32MutexName = this.productService.win32MutexName; if (isWindows && win32MutexName) { diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 76f552ee..3cfab0d7 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -65,6 +65,9 @@ export const defaultProviderSettings = { region: 'us-east-1', // add region setting endpoint: '', // optionally allow overriding default }, + macLocalAFM: { + endpoint: 'http://localhost:9999', + }, } as const @@ -153,6 +156,7 @@ export const defaultModelsOfProvider = { microsoftAzure: [], awsBedrock: [], liteLLM: [], + macLocalAFM: [], // autodetected via afm list endpoint } as const satisfies Record @@ -1449,6 +1453,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 } = { @@ -1474,6 +1494,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi googleVertex: googleVertexSettings, microsoftAzure: microsoftAzureSettings, awsBedrock: awsBedrockSettings, + macLocalAFM: macLocalAFMSettings, } as const diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index c4bfe115..f1e31aca 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -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 } ) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 38497c60..35e66ec3 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -16,9 +16,12 @@ type UnionOfKeys = 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 type CustomProviderSettings = { [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 ` (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 diff --git a/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts b/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts new file mode 100644 index 00000000..7a71f84a --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts @@ -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 => { + 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 => { + 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 => { + 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 => { + + 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; + } + }); +}; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index b4c794e2..4d5b80cf 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -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 From 7dc53cfbc56f54c42b28436486082a979b98f1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 23 Apr 2026 23:37:15 +0200 Subject: [PATCH 2/9] Update version numbers in package.json and product.json to reflect the addition of Apple Foundation Model and Mistral Fetch support. --- package.json | 2 +- product.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e6341c09..23584782 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/product.json b/product.json index b939424b..f3db6c6e 100644 --- a/product.json +++ b/product.json @@ -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", From 6f7adf1946b25bf66d0bdbbbdf5664d75965d951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 23 Apr 2026 23:40:07 +0200 Subject: [PATCH 3/9] Update version in package.json to 1.99.3, reverting from previous version reflecting Apple Foundation Model and Mistral Fetch support. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23584782..e6341c09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-oss-dev", - "version": "2.0.0 - Apple Foundation Model + Mistral Fetch support", + "version": "1.99.3", "distro": "21c8d8ea1e46d97c5639a7cabda6c0e063cc8dd5", "author": { "name": "Microsoft Corporation" From a5320a253832dfc6a05eee66cea10439fdab130c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 00:54:05 +0200 Subject: [PATCH 4/9] Enhance macLocalAFM settings by adding specialToolFormat and updating display name to 'Apple & MLX' --- src/vs/workbench/contrib/void/common/modelCapabilities.ts | 1 + src/vs/workbench/contrib/void/common/voidSettingsTypes.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 57111ee1..8b4d8e41 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -1474,6 +1474,7 @@ const macLocalAFMSettings: VoidStaticProviderInfo = { downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', + specialToolFormat: 'openai-style', reasoningCapabilities: false, }, }, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 35e66ec3..ce3bf2d8 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -110,7 +110,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn return { title: 'AWS Bedrock', } } else if (providerName === 'macLocalAFM') { - return { title: 'Apple Foundation Model', } + return { title: 'Apple & MLX', } } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) From f7257cc7be3bf05ac7c618492912019d6bfe07e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 01:19:24 +0200 Subject: [PATCH 5/9] Refactor macLocalAFM references to apple across model capabilities, refresh model service, and settings types for consistency and clarity. --- .../contrib/void/common/modelCapabilities.ts | 9 +++++---- .../contrib/void/common/refreshModelService.ts | 6 +++--- .../contrib/void/common/voidSettingsTypes.ts | 18 +++++++++--------- .../llmMessage/sendLLMMessage.impl.ts | 4 ++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 8b4d8e41..75f3d1bd 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -61,7 +61,7 @@ export const defaultProviderSettings = { region: 'us-east-1', // add region setting endpoint: '', // optionally allow overriding default }, - macLocalAFM: { + apple: { endpoint: 'http://localhost:9999', }, @@ -155,7 +155,7 @@ export const defaultModelsOfProvider = { microsoftAzure: [], awsBedrock: [], liteLLM: [], - macLocalAFM: [], // autodetected via afm list endpoint + apple: [], // autodetected via afm list endpoint∂ } as const satisfies Record @@ -1464,7 +1464,7 @@ const openRouterSettings: VoidStaticProviderInfo = { // ---------------- MAC LOCAL AFM (Apple Foundation Model / MLX via afm) ---------------- -const macLocalAFMSettings: VoidStaticProviderInfo = { +const appleSettings: VoidStaticProviderInfo = { modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: false }), modelOptions: { 'foundation': { @@ -1505,7 +1505,8 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi googleVertex: googleVertexSettings, microsoftAzure: microsoftAzureSettings, awsBedrock: awsBedrockSettings, - macLocalAFM: macLocalAFMSettings, + // Local Apple model + apple: appleSettings, } as const diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index f1e31aca..d7f9a243 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -47,7 +47,7 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide ollama: ['_didFillInProviderSettings', 'endpoint'], vLLM: ['_didFillInProviderSettings', 'endpoint'], lmStudio: ['_didFillInProviderSettings', 'endpoint'], - macLocalAFM: ['_didFillInProviderSettings', 'endpoint'], + apple: ['_didFillInProviderSettings', 'endpoint'], mistral: ['_didFillInProviderSettings', 'apiKey'], // openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'], } @@ -146,7 +146,7 @@ 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 }, + apple: { state: 'init', timeoutId: null }, mistral: { state: 'init', timeoutId: null }, } @@ -180,7 +180,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ 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 === 'apple') return (model as OpenaiCompatibleModelResponse).id; else if (providerName === 'mistral') return (model as OpenaiCompatibleModelResponse).id; else throw new Error('refreshMode fn: unknown provider', providerName); }), diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index ce3bf2d8..563fb3bd 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -16,11 +16,11 @@ type UnionOfKeys = 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', 'macLocalAFM'] satisfies ProviderName[] // all local names (no API key, endpoint-based) +export const localProviderNames = ['ollama', 'vLLM', 'lmStudio', 'apple'] 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[] +export const refreshableProviderNames = ['ollama', 'vLLM', 'lmStudio', 'apple', 'mistral'] satisfies ProviderName[] type CustomSettingName = UnionOfKeys type CustomProviderSettings = { @@ -109,7 +109,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn else if (providerName === 'awsBedrock') { return { title: 'AWS Bedrock', } } - else if (providerName === 'macLocalAFM') { + else if (providerName === 'apple') { return { title: 'Apple & MLX', } } @@ -134,7 +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 ` (MLX). Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled.' + if (providerName === 'apple') 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 ` (MLX). Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled.' throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`) } @@ -173,7 +173,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName title: providerName === 'ollama' ? 'Endpoint' : providerName === 'vLLM' ? 'Endpoint' : providerName === 'lmStudio' ? 'Endpoint' : - providerName === 'macLocalAFM' ? 'Endpoint' : + providerName === 'apple' ? 'Endpoint' : providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions) providerName === 'googleVertex' ? 'baseURL' : providerName === 'microsoftAzure' ? 'baseURL' : @@ -185,7 +185,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName : providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint - : providerName === 'macLocalAFM' ? defaultProviderSettings.macLocalAFM.endpoint + : providerName === 'apple' ? defaultProviderSettings.apple.endpoint : providerName === 'liteLLM' ? 'http://localhost:4000' : providerName === 'awsBedrock' ? 'http://localhost:4000/v1' : '(never)', @@ -361,10 +361,10 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock), _didFillInProviderSettings: undefined, }, - macLocalAFM: { + apple: { ...defaultCustomSettings, - ...defaultProviderSettings.macLocalAFM, - ...modelInfoOfDefaultModelNames([...defaultModelsOfProvider.macLocalAFM]), + ...defaultProviderSettings.apple, + ...modelInfoOfDefaultModelNames([...defaultModelsOfProvider.apple]), _didFillInProviderSettings: undefined, }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 4d5b80cf..3a1f3b92 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -167,7 +167,7 @@ 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') { + else if (providerName === 'apple') { const thisConfig = settingsOfProvider[providerName] const endpoint = thisConfig.endpoint || 'http://localhost:9999' return new OpenAI({ baseURL: `${endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts }) @@ -942,7 +942,7 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, - macLocalAFM: { + apple: { sendChat: (params) => _sendOpenAICompatibleChat(params), sendFIM: null, list: (params) => _openaiCompatibleList(params), From 943548a7b42ad2272b4792aa687f59cc9144a1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 03:08:35 +0200 Subject: [PATCH 6/9] Enhance Apple model integration by adding MLX endpoint support, updating settings, and refining model fetching logic for improved clarity and functionality. --- .../contrib/void/common/modelCapabilities.ts | 21 ++----- .../contrib/void/common/voidSettingsTypes.ts | 9 ++- .../void/electron-main/afmBackendService.ts | 34 +++++++++-- .../llmMessage/sendLLMMessage.impl.ts | 56 ++++++++++++++++++- 4 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 75f3d1bd..d1c1e6f9 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -62,7 +62,8 @@ export const defaultProviderSettings = { endpoint: '', // optionally allow overriding default }, apple: { - endpoint: 'http://localhost:9999', + endpoint: 'http://localhost:9999', // Apple Foundation Model + mlxEndpoint: 'http://localhost:8080', // MLX models (afm mlx -m -p 8080) }, } as const @@ -137,18 +138,8 @@ export const defaultModelsOfProvider = { 'llama-3.1-8b-instant', // 'qwen-2.5-coder-32b', // preview mode (experimental) ], - mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - 'codestral-latest', - 'devstral-medium-latest', - 'devstral-small-latest', - 'magistral-medium-latest', - 'magistral-small-latest', - 'mistral-large-latest', - 'mistral-medium-latest', - 'mistral-small-latest', - 'ministral-3b-latest', - 'ministral-8b-latest', - 'open-codestral-mamba' + mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview + // Mistral models are autodetected and fetched using the model list endpoint via refreshModelService ], openAICompatible: [], // fallback googleVertex: [], @@ -1472,10 +1463,10 @@ const appleSettings: VoidStaticProviderInfo = { reservedOutputTokenSpace: 2_048, cost: { input: 0, output: 0 }, downloadable: false, - supportsFIM: false, + supportsFIM: true, supportsSystemMessage: 'system-role', specialToolFormat: 'openai-style', - reasoningCapabilities: false, + reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, }, }, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 563fb3bd..40b181ed 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -134,7 +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 === 'apple') 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 ` (MLX). Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled.' + if (providerName === 'apple') return 'Run Apple\'s on-device Foundation Model or any MLX model locally via [afm](https://github.com/scouzi1966/maclocal-api). Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled.\n\n**Foundation Model** (on-device, no download): `afm`\n\n**MLX models for code** (recommended for 8 GB RAM):\n- `afm mlx -m mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` (~4 GB, best for code + autocomplete)\n- `afm mlx -m mlx-community/devstral-small-2505-4bit` (~4 GB, agentic coding)' throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`) } @@ -193,6 +193,12 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName } } + else if (settingName === 'mlxEndpoint') { + return { + title: 'MLX Endpoint', + placeholder: defaultProviderSettings.apple.mlxEndpoint, + } + } else if (settingName === 'headersJSON') { return { title: 'Custom Headers', placeholder: '{ "X-Request-Id": "..." }' } } @@ -366,6 +372,7 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...defaultProviderSettings.apple, ...modelInfoOfDefaultModelNames([...defaultModelsOfProvider.apple]), _didFillInProviderSettings: undefined, + mlxEndpoint: defaultProviderSettings.apple.mlxEndpoint, }, } diff --git a/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts b/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts index 7a71f84a..b7681f47 100644 --- a/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts +++ b/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts @@ -8,7 +8,23 @@ import { createServer } from 'net'; import { promisify } from 'util'; const exec = promisify(execCb); -const AFM_DEFAULT_PORT = 9999; +const AFM_FOUNDATION_PORT = 9999; // Apple Foundation Model +const AFM_MLX_PORT = 8080; // MLX models (afm mlx -m -p 8080) + +// Electron apps don't source the shell, so PATH is minimal (/usr/bin:/bin). +// We enrich it with common Homebrew paths so `afm` and `brew` can be found. +const HOMEBREW_PATHS = [ + '/opt/homebrew/bin', // Apple Silicon + '/usr/local/bin', // Intel Mac + '/opt/homebrew/sbin', + '/usr/local/sbin', +]; + +const enrichedEnv = (): NodeJS.ProcessEnv => { + const currentPath = process.env.PATH ?? ''; + const extra = HOMEBREW_PATHS.filter(p => !currentPath.includes(p)).join(':'); + return { ...process.env, PATH: extra ? `${extra}:${currentPath}` : currentPath }; +}; /** * Returns true if something is already listening on the given port. @@ -25,11 +41,11 @@ const isPortInUse = (port: number): Promise => { }; /** - * Returns true if the given command is available in PATH. + * Returns true if the given command is available in PATH (including Homebrew paths). */ const isCommandAvailable = async (cmd: string): Promise => { try { - await exec(`which ${cmd}`); + await exec(`which ${cmd}`, { env: enrichedEnv() }); return true; } catch { return false; @@ -49,7 +65,7 @@ const installAfmViaBrew = async (log: (msg: string) => void): Promise = log('[Void] afm: not found — installing via Homebrew (this may take a moment)…'); try { - await exec('brew install scouzi1966/afm/afm'); + await exec('brew install scouzi1966/afm/afm', { env: enrichedEnv() }); log('[Void] afm: installed successfully via Homebrew'); return true; } catch (e) { @@ -75,12 +91,18 @@ export const startAfmIfNeeded = async ( return; // afm is macOS-only } - const portInUse = await isPortInUse(AFM_DEFAULT_PORT); + const portInUse = await isPortInUse(AFM_FOUNDATION_PORT); if (portInUse) { log('[Void] afm: port 9999 already in use — using existing afm instance'); return; } + // Check if an MLX model is already running on port 8080 + const mlxPortInUse = await isPortInUse(AFM_MLX_PORT); + if (mlxPortInUse) { + log('[Void] afm: MLX model detected on port 8080'); + } + // Auto-install if afm is not in PATH const afmAvailable = await isCommandAvailable('afm'); if (!afmAvailable) { @@ -97,7 +119,7 @@ export const startAfmIfNeeded = async ( afmProcess = spawn('afm', ['-g'], { detached: false, stdio: 'ignore', - env: { ...process.env }, + env: enrichedEnv(), }); } catch (e) { log(`[Void] afm: could not start: ${e}`); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 3a1f3b92..eea4ed3a 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -169,6 +169,7 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ } else if (providerName === 'apple') { const thisConfig = settingsOfProvider[providerName] + // endpoint may have been pre-patched by _appleSettingsForModel to point to the right port const endpoint = thisConfig.endpoint || 'http://localhost:9999' return new OpenAI({ baseURL: `${endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts }) } @@ -400,6 +401,54 @@ type OpenAIModel = { object: 'model'; owned_by: string; } +// Returns settingsOfProvider with apple.endpoint patched to the right URL based on model name. +// - 'foundation' → endpoint (port 9999, Apple Foundation Model) +// - anything else → mlxEndpoint (port 8080, MLX models) with fallback to endpoint +const _appleSettingsForModel = (settingsOfProvider: SettingsOfProvider, modelName: string): SettingsOfProvider => { + const config = settingsOfProvider.apple + const isFoundation = modelName === 'foundation' + const targetEndpoint = isFoundation + ? (config.endpoint || 'http://localhost:9999') + : (config.mlxEndpoint || config.endpoint || 'http://localhost:8080') + return { ...settingsOfProvider, apple: { ...config, endpoint: targetEndpoint } } +} + +// Lists models from both AFM endpoints and merges them. +const _appleList = async ({ onSuccess, onError, settingsOfProvider, providerName }: ListParams_Internal) => { + const config = settingsOfProvider.apple + const mainEndpoint = config.endpoint || 'http://localhost:9999' + const mlxEndpoint = config.mlxEndpoint || 'http://localhost:8080' + + const fetchModels = async (baseURL: string): Promise => { + const openai = new OpenAI({ baseURL: `${baseURL}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true }) + const response = await openai.models.list() + const models: OpenAIModel[] = [...response.data] + while (response.hasNextPage()) { + models.push(...(await response.getNextPage()).data) + } + return models + } + + // Query both endpoints in parallel; ignore failures from either one + const [mainResult, mlxResult] = await Promise.allSettled([ + fetchModels(mainEndpoint), + mainEndpoint !== mlxEndpoint ? fetchModels(mlxEndpoint) : Promise.resolve([]), + ]) + + const allModels: OpenAIModel[] = [] + if (mainResult.status === 'fulfilled') allModels.push(...mainResult.value) + if (mlxResult.status === 'fulfilled') allModels.push(...mlxResult.value) + + if (allModels.length === 0 && mainResult.status === 'rejected') { + onError({ error: mainResult.reason + '' }) + } else { + // Deduplicate by model id + const seen = new Set() + const unique = allModels.filter(m => { if (seen.has(m.id)) return false; seen.add(m.id); return true }) + onSuccess({ models: unique }) + } +} + const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) @@ -943,9 +992,12 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, apple: { - sendChat: (params) => _sendOpenAICompatibleChat(params), + sendChat: (params) => _sendOpenAICompatibleChat({ + ...params, + settingsOfProvider: _appleSettingsForModel(params.settingsOfProvider, params.modelName), + }), sendFIM: null, - list: (params) => _openaiCompatibleList(params), + list: (params) => _appleList(params), }, } satisfies CallFnOfProvider From 5a8de1a5c3d7c9ced9888241fbd1e8de3b93681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 03:27:26 +0200 Subject: [PATCH 7/9] Refine Apple model endpoint handling by introducing a mapping for model IDs to their respective endpoints, enhancing the logic for routing requests based on model listings, and improving fallback mechanisms for unlisted models. --- .../void/common/refreshModelService.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index d7f9a243..0c6fcc20 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -47,7 +47,7 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide ollama: ['_didFillInProviderSettings', 'endpoint'], vLLM: ['_didFillInProviderSettings', 'endpoint'], lmStudio: ['_didFillInProviderSettings', 'endpoint'], - apple: ['_didFillInProviderSettings', 'endpoint'], + apple: ['_didFillInProviderSettings', 'endpoint', 'mlxEndpoint'], mistral: ['_didFillInProviderSettings', 'apiKey'], // openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'], } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index eea4ed3a..1e029f52 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -401,19 +401,22 @@ type OpenAIModel = { object: 'model'; owned_by: string; } -// Returns settingsOfProvider with apple.endpoint patched to the right URL based on model name. -// - 'foundation' → endpoint (port 9999, Apple Foundation Model) -// - anything else → mlxEndpoint (port 8080, MLX models) with fallback to endpoint +// Maps each Apple model id to the endpoint it was last seen on (populated by _appleList). +// This lets _appleSettingsForModel route requests correctly without relying on name conventions. +const _appleModelEndpointMap = new Map() + +// Returns settingsOfProvider with apple.endpoint patched to the URL the model was listed from. +// Falls back to the 'foundation' name convention if the model hasn't been listed yet. const _appleSettingsForModel = (settingsOfProvider: SettingsOfProvider, modelName: string): SettingsOfProvider => { const config = settingsOfProvider.apple - const isFoundation = modelName === 'foundation' - const targetEndpoint = isFoundation - ? (config.endpoint || 'http://localhost:9999') - : (config.mlxEndpoint || config.endpoint || 'http://localhost:8080') + const mainEndpoint = config.endpoint || 'http://localhost:9999' + const mlxEndpoint = config.mlxEndpoint || config.endpoint || 'http://localhost:8080' + const targetEndpoint = _appleModelEndpointMap.get(modelName) + ?? (modelName === 'foundation' ? mainEndpoint : mlxEndpoint) return { ...settingsOfProvider, apple: { ...config, endpoint: targetEndpoint } } } -// Lists models from both AFM endpoints and merges them. +// Lists models from both AFM endpoints, records their source, and merges them. const _appleList = async ({ onSuccess, onError, settingsOfProvider, providerName }: ListParams_Internal) => { const config = settingsOfProvider.apple const mainEndpoint = config.endpoint || 'http://localhost:9999' @@ -436,8 +439,14 @@ const _appleList = async ({ onSuccess, onError, settingsOfProvider, providerName ]) const allModels: OpenAIModel[] = [] - if (mainResult.status === 'fulfilled') allModels.push(...mainResult.value) - if (mlxResult.status === 'fulfilled') allModels.push(...mlxResult.value) + if (mainResult.status === 'fulfilled') { + for (const m of mainResult.value) _appleModelEndpointMap.set(m.id, mainEndpoint) + allModels.push(...mainResult.value) + } + if (mlxResult.status === 'fulfilled') { + for (const m of mlxResult.value) _appleModelEndpointMap.set(m.id, mlxEndpoint) + allModels.push(...mlxResult.value) + } if (allModels.length === 0 && mainResult.status === 'rejected') { onError({ error: mainResult.reason + '' }) From 03305a17e3d2456b0ef5f20372abbaee69b46f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 03:32:33 +0200 Subject: [PATCH 8/9] Add mlxEndpoint to defaultCustomSettings in voidSettingsTypes.ts for enhanced configuration support. --- src/vs/workbench/contrib/void/common/voidSettingsTypes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 40b181ed..263e28f6 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -252,6 +252,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName const defaultCustomSettings: Record = { apiKey: undefined, endpoint: undefined, + mlxEndpoint: undefined, region: undefined, // googleVertex project: undefined, azureApiVersion: undefined, From 4a4cffcfe3ababa078c527d6c3f78d58ebb3effe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 24 Apr 2026 03:43:12 +0200 Subject: [PATCH 9/9] Refactor model mapping logic in RefreshModelService to use flatMap for improved handling of provider-specific model IDs, including conditional filtering for Mistral models. --- .../void/common/refreshModelService.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index 0c6fcc20..5507c227 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -176,14 +176,17 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // set the models to the detected models 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 if (providerName === 'apple') return (model as OpenaiCompatibleModelResponse).id; - else if (providerName === 'mistral') return (model as OpenaiCompatibleModelResponse).id; - else throw new Error('refreshMode fn: unknown provider', providerName); - }), + models.flatMap(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 if (providerName === 'apple') return [(model as OpenaiCompatibleModelResponse).id]; + else if (providerName === 'mistral') { + const id = (model as OpenaiCompatibleModelResponse).id; + return id.endsWith('-latest') ? [id] : []; + } + else throw new Error('refreshMode fn: unknown provider', providerName); + }), { enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire } )