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", 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 f5d4dfb4..d1c1e6f9 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -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 -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 @@ -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: ['', ''] }, + }, + }, +} + // ---------------- 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 diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index c4bfe115..5507c227 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'], + 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 } ) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 38497c60..263e28f6 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', '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 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 === '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 = { 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 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..b7681f47 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/afmBackendService.ts @@ -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 -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 => { + 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 => { + 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 => { + 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 => { + + 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; + } + }); +}; 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..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 @@ -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() + +// 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) => { + 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') { + 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() + 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 }) @@ -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