mirror of
https://github.com/voideditor/void
synced 2026-05-22 08:58:26 +00:00
commit
912d668d95
7 changed files with 313 additions and 32 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ export const defaultProviderSettings = {
|
|||
region: 'us-east-1', // add region setting
|
||||
endpoint: '', // optionally allow overriding default
|
||||
},
|
||||
apple: {
|
||||
endpoint: 'http://localhost:9999', // Apple Foundation Model
|
||||
mlxEndpoint: 'http://localhost:8080', // MLX models (afm mlx -m <model> -p 8080)
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
|
|
@ -134,24 +138,15 @@ 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: [],
|
||||
microsoftAzure: [],
|
||||
awsBedrock: [],
|
||||
liteLLM: [],
|
||||
apple: [], // autodetected via afm list endpoint∂
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
|
@ -1459,6 +1454,23 @@ const openRouterSettings: VoidStaticProviderInfo = {
|
|||
|
||||
|
||||
|
||||
// ---------------- MAC LOCAL AFM (Apple Foundation Model / MLX via afm) ----------------
|
||||
const appleSettings: VoidStaticProviderInfo = {
|
||||
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: false }),
|
||||
modelOptions: {
|
||||
'foundation': {
|
||||
contextWindow: 4_096,
|
||||
reservedOutputTokenSpace: 2_048,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
specialToolFormat: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = {
|
||||
|
|
@ -1484,6 +1496,8 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
awsBedrock: awsBedrockSettings,
|
||||
// Local Apple model
|
||||
apple: appleSettings,
|
||||
} as const
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
apple: ['_didFillInProviderSettings', 'endpoint', 'mlxEndpoint'],
|
||||
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 },
|
||||
apple: { 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)
|
||||
|
|
@ -170,12 +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 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 }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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', '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', 'apple', '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 === 'apple') {
|
||||
return { title: 'Apple & MLX', }
|
||||
}
|
||||
|
||||
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 === '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}"`)
|
||||
}
|
||||
|
|
@ -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 === 'apple' ? '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 === 'apple' ? defaultProviderSettings.apple.endpoint
|
||||
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
|
||||
: '(never)',
|
||||
|
|
@ -184,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": "..." }' }
|
||||
}
|
||||
|
|
@ -237,6 +252,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
mlxEndpoint: undefined,
|
||||
region: undefined, // googleVertex
|
||||
project: undefined,
|
||||
azureApiVersion: undefined,
|
||||
|
|
@ -352,6 +368,13 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
apple: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.apple,
|
||||
...modelInfoOfDefaultModelNames([...defaultModelsOfProvider.apple]),
|
||||
_didFillInProviderSettings: undefined,
|
||||
mlxEndpoint: defaultProviderSettings.apple.mlxEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -385,8 +408,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
|
||||
|
|
|
|||
153
src/vs/workbench/contrib/void/electron-main/afmBackendService.ts
Normal file
153
src/vs/workbench/contrib/void/electron-main/afmBackendService.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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_FOUNDATION_PORT = 9999; // Apple Foundation Model
|
||||
const AFM_MLX_PORT = 8080; // MLX models (afm mlx -m <model> -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.
|
||||
*/
|
||||
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 (including Homebrew paths).
|
||||
*/
|
||||
const isCommandAvailable = async (cmd: string): Promise<boolean> => {
|
||||
try {
|
||||
await exec(`which ${cmd}`, { env: enrichedEnv() });
|
||||
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', { env: enrichedEnv() });
|
||||
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_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) {
|
||||
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: enrichedEnv(),
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -167,6 +167,12 @@ 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 === '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 })
|
||||
}
|
||||
|
||||
else throw new Error(`Void providerName was invalid: ${providerName}.`)
|
||||
}
|
||||
|
|
@ -395,6 +401,63 @@ type OpenAIModel = {
|
|||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
// 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<string, string>()
|
||||
|
||||
// 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 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, records their source, and merges them.
|
||||
const _appleList = async ({ onSuccess, onError, settingsOfProvider, providerName }: ListParams_Internal<OpenAIModel>) => {
|
||||
const config = settingsOfProvider.apple
|
||||
const mainEndpoint = config.endpoint || 'http://localhost:9999'
|
||||
const mlxEndpoint = config.mlxEndpoint || 'http://localhost:8080'
|
||||
|
||||
const fetchModels = async (baseURL: string): Promise<OpenAIModel[]> => {
|
||||
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') {
|
||||
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 + '' })
|
||||
} else {
|
||||
// Deduplicate by model id
|
||||
const seen = new Set<string>()
|
||||
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<OpenAIModel>) => {
|
||||
const onSuccess = ({ models }: { models: OpenAIModel[] }) => {
|
||||
onSuccess_({ models })
|
||||
|
|
@ -878,7 +941,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 +1000,14 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
apple: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat({
|
||||
...params,
|
||||
settingsOfProvider: _appleSettingsForModel(params.settingsOfProvider, params.modelName),
|
||||
}),
|
||||
sendFIM: null,
|
||||
list: (params) => _appleList(params),
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue