mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
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.
This commit is contained in:
parent
bf80a00b30
commit
9c98e1c688
6 changed files with 110 additions and 36 deletions
|
|
@ -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 <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 text-void-fg-3 text-sm list-decimal select-text mb-4'>
|
||||
<div><ChatMarkdownRender string={`Apple (one model: \`foundation\`)`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`1. Requires macOS 26+, Apple Silicon, and Apple Intelligence enabled. Void can install [\`afm\`](https://github.com/scouzi1966/maclocal-api) via Homebrew (toggle in Settings → Models).`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`2. Only one **autodetected** entry (\`foundation\`): the on-device model exposed by \`afm\`.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`3. **Fine-tuned adapter**: \`afm -a ./my-adapter.fmadapter -p 9998\`, then set Void’s endpoint to \`http://127.0.0.1:9998\` and refresh.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`4. **Other models (Llama, Qwen, etc.)**: use **MLX** or **Ollama** — Apple FM only serves Apple’s built-in Foundation model.`} chatMessageLocation={undefined} /></div>
|
||||
<div><ChatMarkdownRender string={`apple — [maclocal-api](https://github.com/scouzi1966/maclocal-api) (\`afm\`)`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`1. Requires macOS 26+, Apple Silicon, and Apple Intelligence. Void auto-installs \`afm\` (Homebrew: \`brew tap scouzi1966/afm && brew install scouzi1966/afm/afm\`, or pip: \`pip install macafm\`).`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`2. Default server: \`afm -p 9999\` → endpoint \`http://127.0.0.1:9999/v1\`, model id \`foundation\`.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`3. **LoRA adapter**: \`afm -a ./my-adapter.fmadapter -p 9998\`, then set Void’s endpoint to \`http://127.0.0.1:9998\` and refresh.`} chatMessageLocation={undefined} /></div>
|
||||
<div className='pl-6'><ChatMarkdownRender string={`4. **Other local models**: use **MLX** (\`mlx_lm.server\`) or \`afm mlx -m <hf-repo>\` from maclocal-api — this provider is only Apple’s on-device Foundation model.`} chatMessageLocation={undefined} /></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 <model>` 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;
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
|
|
@ -102,6 +115,60 @@ export class AppleFoundationModelsMainService implements IAppleFoundationModelsM
|
|||
this._spawnedByVoid = false;
|
||||
}
|
||||
|
||||
private async _tryInstallViaHomebrew(log: string[]): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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<string | null> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue