From 9c98e1c68824175a3e33736314b278add41e0303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Wed, 20 May 2026 16:37:36 +0200 Subject: [PATCH] Refactor Apple Foundation Models integration and update setup instructions - Enhanced comments and documentation for macOS Apple Foundation Models (afm) auto-installation and server setup. - Updated user-facing text in settings to clarify installation methods via Homebrew and pip. - Added constants for Homebrew and pip installation details to improve maintainability. - Improved logging messages for server status and installation processes. --- .../react/src/void-settings-tsx/Settings.tsx | 12 +- .../contrib/void/browser/void.contribution.ts | 2 +- .../void/common/appleFoundationModelsTypes.ts | 7 ++ .../contrib/void/common/modelCapabilities.ts | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 4 +- .../appleFoundationModelsMainService.ts | 119 ++++++++++++++---- 6 files changed, 110 insertions(+), 36 deletions(-) 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',