diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
index d14e6714..6280afb8 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
@@ -829,7 +829,7 @@ export const AutoSetupAppleFoundationModelsToggle = () => {
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.'
+ text='On macOS: install maclocal-api (afm) via Homebrew or pip, start the server on port 9999, and enable model foundation.'
/>
}
@@ -900,11 +900,11 @@ export const MlxSetupInstructions = () => {
export const AppleFoundationModelsSetupInstructions = () => {
if (os !== 'mac') return null
return
-
-
-
-
-
+
+
+
+
+
\` from maclocal-api — this provider is only Apple’s on-device Foundation model.`} chatMessageLocation={undefined} />
}
diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts
index 0c069174..9080283b 100644
--- a/src/vs/workbench/contrib/void/browser/void.contribution.ts
+++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts
@@ -75,7 +75,7 @@ import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
-// Apple Foundation Models (macOS): auto-install afm + start server
+// apple (macOS): maclocal-api (afm) auto-install + server
import '../common/appleFoundationModelsService.js'
import './appleFoundationModelsWorkbenchContrib.js'
diff --git a/src/vs/workbench/contrib/void/common/appleFoundationModelsTypes.ts b/src/vs/workbench/contrib/void/common/appleFoundationModelsTypes.ts
index 3ad0562e..98a0179f 100644
--- a/src/vs/workbench/contrib/void/common/appleFoundationModelsTypes.ts
+++ b/src/vs/workbench/contrib/void/common/appleFoundationModelsTypes.ts
@@ -8,6 +8,13 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
export const APPLE_FOUNDATION_MODELS_DEFAULT_ENDPOINT = 'http://127.0.0.1:9999';
export const APPLE_FOUNDATION_MODELS_DEFAULT_PORT = 9999;
+/** https://github.com/scouzi1966/maclocal-api — OpenAI-compatible `afm` server for Apple Foundation Models */
+export const MACLOCAL_API_REPO_URL = 'https://github.com/scouzi1966/maclocal-api';
+export const AFM_DEFAULT_MODEL_ID = 'foundation';
+export const AFM_HOMEBREW_TAP = 'scouzi1966/afm';
+export const AFM_HOMEBREW_FORMULA = 'scouzi1966/afm/afm';
+export const AFM_PIP_PACKAGE = 'macafm';
+
export type AppleFoundationModelsEnsureAction =
| 'already-running'
| 'started'
diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts
index 54a951e9..94c79cd3 100644
--- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts
+++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts
@@ -1330,7 +1330,7 @@ const appleFoundationModelCapabilities = {
}
const appleFoundationModelsModelOptions = {
- 'foundation': { // Apple on-device model via afm — https://github.com/scouzi1966/maclocal-api
+ 'foundation': { // maclocal-api afm — https://github.com/scouzi1966/maclocal-api
...appleFoundationModelCapabilities,
},
} as const satisfies { [s: string]: VoidStaticModelInfo }
diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
index 17fbf9c1..85e03b97 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 === '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 === 'appleFoundationModels') return 'On-device model `foundation` via [maclocal-api](https://github.com/scouzi1966/maclocal-api) (`afm` on port 9999). Void can install via Homebrew or `pip install macafm`. For MLX models on the same stack, run `afm mlx -m ` separately or use the **MLX** provider.'
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}"`)
@@ -464,7 +464,7 @@ 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 */
+ /** macOS only: install maclocal-api (`afm`) via Homebrew or pip and start the server */
autoSetupAppleFoundationModels: boolean;
/** macOS only: install `mlx-lm` via pip and start mlx_lm.server */
autoSetupMlx: boolean;
diff --git a/src/vs/workbench/contrib/void/electron-main/appleFoundationModelsMainService.ts b/src/vs/workbench/contrib/void/electron-main/appleFoundationModelsMainService.ts
index d4979078..0403397a 100644
--- a/src/vs/workbench/contrib/void/electron-main/appleFoundationModelsMainService.ts
+++ b/src/vs/workbench/contrib/void/electron-main/appleFoundationModelsMainService.ts
@@ -8,9 +8,13 @@ import { promisify } from 'util';
import { exec as _exec } from 'child_process';
import { isMacintosh } from '../../../../base/common/platform.js';
import {
+ AFM_HOMEBREW_FORMULA,
+ AFM_HOMEBREW_TAP,
+ AFM_PIP_PACKAGE,
APPLE_FOUNDATION_MODELS_DEFAULT_PORT,
AppleFoundationModelsEnsureResult,
IAppleFoundationModelsMainService,
+ MACLOCAL_API_REPO_URL,
} from '../common/appleFoundationModelsTypes.js';
const exec = promisify(_exec);
@@ -33,7 +37,7 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
}
if (await this._isServerUp(endpoint)) {
- log.push(`Serveur déjà actif sur ${endpoint}`);
+ log.push(`maclocal-api (afm) already running at ${endpoint}`);
return { ok: true, endpoint, action: 'already-running', log };
}
@@ -41,35 +45,39 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
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' };
+ log.push('`afm` not found (maclocal-api).');
+ return {
+ ok: false,
+ reason: 'afm-missing',
+ log,
+ errorMessage: `Install from ${MACLOCAL_API_REPO_URL} — Homebrew: brew tap ${AFM_HOMEBREW_TAP} && brew install ${AFM_HOMEBREW_FORMULA} — or pip: pip install ${AFM_PIP_PACKAGE}`,
+ };
}
- 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 });
+ const installedViaBrew = await this._tryInstallViaHomebrew(log);
+ if (installedViaBrew) {
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 };
+ } else {
+ const installedViaPip = await this._tryInstallViaPip(log);
+ if (installedViaPip) {
+ didInstall = true;
+ } else {
+ return {
+ ok: false,
+ reason: 'install-failed',
+ log,
+ errorMessage: `Could not install afm. See ${MACLOCAL_API_REPO_URL}`,
+ };
+ }
}
afmPath = await this._whichAfm();
if (!afmPath) {
- log.push('afm toujours introuvable après installation.');
+ log.push('`afm` still not on PATH after install.');
return { ok: false, reason: 'afm-missing', log };
}
} else {
- log.push(`afm trouvé : ${afmPath}`);
+ log.push(`Found afm: ${afmPath}`);
}
if (options.startServer) {
@@ -79,14 +87,19 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
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}`);
+ log.push(`maclocal-api ready at ${endpoint} (model: foundation)`);
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.` };
+ log.push('Timed out waiting for afm.');
+ return {
+ ok: false,
+ reason: 'server-timeout',
+ log,
+ errorMessage: `Server did not respond at ${endpoint}. Run manually: afm -p ${port} -H 127.0.0.1`,
+ };
}
async stopServerIfSpawnedByVoid(): Promise {
@@ -102,6 +115,60 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
this._spawnedByVoid = false;
}
+ private async _tryInstallViaHomebrew(log: string[]): Promise {
+ const brewPath = await this._whichBrew();
+ if (!brewPath) {
+ log.push('Homebrew not found; will try pip install macafm.');
+ return false;
+ }
+
+ log.push(`Installing afm via Homebrew (${AFM_HOMEBREW_FORMULA})…`);
+ try {
+ await exec(`"${brewPath}" tap ${AFM_HOMEBREW_TAP}`, { timeout: 120_000 });
+ await exec(`"${brewPath}" install ${AFM_HOMEBREW_FORMULA}`, { timeout: 600_000 });
+ log.push('Homebrew install finished.');
+ return true;
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ log.push(`Homebrew install failed: ${msg}`);
+ return false;
+ }
+ }
+
+ private async _tryInstallViaPip(log: string[]): Promise {
+ const pythonPath = await this._whichPython3();
+ if (!pythonPath) {
+ log.push('python3 not found; cannot pip install macafm.');
+ return false;
+ }
+
+ log.push(`Installing ${AFM_PIP_PACKAGE} via pip (${MACLOCAL_API_REPO_URL})…`);
+ try {
+ await exec(`"${pythonPath}" -m pip install --upgrade ${AFM_PIP_PACKAGE}`, { timeout: 600_000 });
+ log.push('pip install finished.');
+ return true;
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ log.push(`pip install failed: ${msg}`);
+ return false;
+ }
+ }
+
+ private async _whichPython3(): Promise {
+ 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 _whichAfm(): Promise {
try {
const { stdout } = await exec('which afm', { timeout: 5_000 });
@@ -131,7 +198,7 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
return true;
}
} catch {
- // try models next
+ // try /v1/models next
} finally {
clearTimeout(timeout);
}
@@ -150,11 +217,11 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
private async _startServer(afmPath: string, port: number, log: string[]): Promise {
if (this._child && !this._child.killed) {
- log.push('Processus afm Void déjà en cours.');
+ log.push('Void afm process already running.');
return;
}
- log.push(`Démarrage de afm sur le port ${port}…`);
+ log.push(`Starting maclocal-api: afm -p ${port} -H 127.0.0.1…`);
const child = spawn(afmPath, ['-p', String(port), '-H', '127.0.0.1'], {
detached: true,
stdio: 'ignore',