From 961ee2232780aefa54a2be3ab79c0ec8a0b825dd Mon Sep 17 00:00:00 2001 From: Kejiang Ma Date: Mon, 29 Sep 2025 18:36:54 +0800 Subject: [PATCH 1/2] feat: add new provider intel OVMS(openvino model server) (#9853) * add new provider: OVMS(openvino model server) Signed-off-by: Ma, Kejiang * remove useless comments * add note: support windows only * fix eslint error; add migrate for ovms provider Signed-off-by: Ma, Kejiang * fix ci error after rebase Signed-off-by: Ma, Kejiang * modifications base on reviewers' comments Signed-off-by: Ma, Kejiang * show intel-ovms provider only on windows and intel cpu Signed-off-by: Ma, Kejiang * complete i18n for intel ovms Signed-off-by: Ma, Kejiang * update ovms 2025.3; apply patch for model qwen3-8b on local Signed-off-by: Ma, Kejiang * fix lint issues Signed-off-by: Ma, Kejiang * fix issues for format, type checking Signed-off-by: Ma, Kejiang * remove test code Signed-off-by: Ma, Kejiang * fix issues after rebase Signed-off-by: Ma, Kejiang --------- Signed-off-by: Ma, Kejiang --- packages/shared/IpcChannel.ts | 11 + resources/scripts/download.js | 39 +- resources/scripts/install-ovms.js | 177 ++++++ src/main/ipc.ts | 15 + src/main/services/OvmsManager.ts | 586 ++++++++++++++++++ src/preload/index.ts | 14 +- .../aiCore/legacy/clients/ApiClientFactory.ts | 7 + .../aiCore/legacy/clients/ovms/OVMSClient.ts | 56 ++ .../src/assets/images/providers/intel.png | Bin 0 -> 3706 bytes src/renderer/src/config/models/default.ts | 1 + src/renderer/src/config/providers.ts | 22 + src/renderer/src/i18n/label.ts | 1 + src/renderer/src/i18n/locales/en-us.json | 53 ++ src/renderer/src/i18n/locales/zh-cn.json | 53 ++ src/renderer/src/i18n/locales/zh-tw.json | 53 ++ src/renderer/src/i18n/translate/el-gr.json | 53 ++ src/renderer/src/i18n/translate/es-es.json | 53 ++ src/renderer/src/i18n/translate/fr-fr.json | 53 ++ src/renderer/src/i18n/translate/ja-jp.json | 53 ++ src/renderer/src/i18n/translate/pt-pt.json | 53 ++ src/renderer/src/i18n/translate/ru-ru.json | 53 ++ .../ModelList/DownloadOVMSModelPopup.tsx | 353 +++++++++++ .../ProviderSettings/ModelList/ModelList.tsx | 18 +- .../ProviderSettings/OVMSSettings.tsx | 170 +++++ .../ProviderSettings/ProviderList.tsx | 6 + .../ProviderSettings/ProviderSetting.tsx | 2 + src/renderer/src/store/migrate.ts | 9 + src/renderer/src/types/index.ts | 1 + 28 files changed, 1960 insertions(+), 5 deletions(-) create mode 100644 resources/scripts/install-ovms.js create mode 100644 src/main/services/OvmsManager.ts create mode 100644 src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts create mode 100644 src/renderer/src/assets/images/providers/intel.png create mode 100644 src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx create mode 100644 src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 0eb0dd2797..1481d5acad 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -34,6 +34,7 @@ export enum IpcChannel { App_GetBinaryPath = 'app:get-binary-path', App_InstallUvBinary = 'app:install-uv-binary', App_InstallBunBinary = 'app:install-bun-binary', + App_InstallOvmsBinary = 'app:install-ovms-binary', App_LogToMain = 'app:log-to-main', App_SaveData = 'app:save-data', App_GetDiskInfo = 'app:get-disk-info', @@ -220,6 +221,7 @@ export enum IpcChannel { // system System_GetDeviceType = 'system:getDeviceType', System_GetHostname = 'system:getHostname', + System_GetCpuName = 'system:getCpuName', // DevTools System_ToggleDevTools = 'system:toggleDevTools', @@ -330,6 +332,15 @@ export enum IpcChannel { // OCR OCR_ocr = 'ocr:ocr', + // OVMS + Ovms_AddModel = 'ovms:add-model', + Ovms_StopAddModel = 'ovms:stop-addmodel', + Ovms_GetModels = 'ovms:get-models', + Ovms_IsRunning = 'ovms:is-running', + Ovms_GetStatus = 'ovms:get-status', + Ovms_RunOVMS = 'ovms:run-ovms', + Ovms_StopOVMS = 'ovms:stop-ovms', + // CherryAI Cherryai_GetSignature = 'cherryai:get-signature' } diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 2e9d83a9e7..3a5766d0b2 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -1,5 +1,7 @@ const https = require('https') const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') /** * Downloads a file from a URL with redirect handling @@ -32,4 +34,39 @@ async function downloadWithRedirects(url, destinationPath) { }) } -module.exports = { downloadWithRedirects } +/** + * Downloads a file using PowerShell Invoke-WebRequest command + * @param {string} url The URL to download from + * @param {string} destinationPath The path to save the file to + * @returns {Promise} Promise that resolves to true if download succeeds + */ +async function downloadWithPowerShell(url, destinationPath) { + return new Promise((resolve, reject) => { + try { + // Only support windows platform for PowerShell download + if (process.platform !== 'win32') { + return reject(new Error('PowerShell download is only supported on Windows')) + } + + const outputDir = path.dirname(destinationPath) + fs.mkdirSync(outputDir, { recursive: true }) + + // PowerShell command to download the file with progress disabled for faster download + const psCommand = `powershell -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest '${url}' -OutFile '${destinationPath}'"` + + console.log(`Downloading with PowerShell: ${url}`) + execSync(psCommand, { stdio: 'inherit' }) + + if (fs.existsSync(destinationPath)) { + console.log(`Download completed: ${destinationPath}`) + resolve(true) + } else { + reject(new Error('Download failed: File not found after download')) + } + } catch (error) { + reject(new Error(`PowerShell download failed: ${error.message}`)) + } + }) +} + +module.exports = { downloadWithRedirects, downloadWithPowerShell } diff --git a/resources/scripts/install-ovms.js b/resources/scripts/install-ovms.js new file mode 100644 index 0000000000..57710e43f6 --- /dev/null +++ b/resources/scripts/install-ovms.js @@ -0,0 +1,177 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') +const { execSync } = require('child_process') +const { downloadWithPowerShell } = require('./download') + +// Base URL for downloading OVMS binaries +const OVMS_PKG_NAME = 'ovms250911.zip' +const OVMS_RELEASE_BASE_URL = [`https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/${OVMS_PKG_NAME}`] + +/** + * Downloads and extracts the OVMS binary for the specified platform + */ +async function downloadOvmsBinary() { + // Create output directory structure - OVMS goes into its own subdirectory + const csDir = path.join(os.homedir(), '.cherrystudio') + + // Ensure directories exist + fs.mkdirSync(csDir, { recursive: true }) + + const csOvmsDir = path.join(csDir, 'ovms') + // Delete existing OVMS directory if it exists + if (fs.existsSync(csOvmsDir)) { + fs.rmSync(csOvmsDir, { recursive: true }) + } + + const tempdir = os.tmpdir() + const tempFilename = path.join(tempdir, 'ovms.zip') + + // Try each URL until one succeeds + let downloadSuccess = false + let lastError = null + + for (let i = 0; i < OVMS_RELEASE_BASE_URL.length; i++) { + const downloadUrl = OVMS_RELEASE_BASE_URL[i] + console.log(`Attempting download from URL ${i + 1}/${OVMS_RELEASE_BASE_URL.length}: ${downloadUrl}`) + + try { + console.log(`Downloading OVMS from ${downloadUrl} to ${tempFilename}...`) + + // Try PowerShell download first, fallback to Node.js download if it fails + await downloadWithPowerShell(downloadUrl, tempFilename) + + // If we get here, download was successful + downloadSuccess = true + console.log(`Successfully downloaded from: ${downloadUrl}`) + break + } catch (error) { + console.warn(`Download failed from ${downloadUrl}: ${error.message}`) + lastError = error + + // Clean up failed download file if it exists + if (fs.existsSync(tempFilename)) { + try { + fs.unlinkSync(tempFilename) + } catch (cleanupError) { + console.warn(`Failed to clean up temporary file: ${cleanupError.message}`) + } + } + + // Continue to next URL if this one failed + if (i < OVMS_RELEASE_BASE_URL.length - 1) { + console.log(`Trying next URL...`) + } + } + } + + // Check if any download succeeded + if (!downloadSuccess) { + console.error(`All download URLs failed. Last error: ${lastError?.message || 'Unknown error'}`) + return 103 + } + + try { + console.log(`Extracting to ${csDir}...`) + + // Use tar.exe to extract the ZIP file + console.log(`Extracting OVMS to ${csDir}...`) + execSync(`tar -xf ${tempFilename} -C ${csDir}`, { stdio: 'inherit' }) + console.log(`OVMS extracted to ${csDir}`) + + // Clean up temporary file + fs.unlinkSync(tempFilename) + console.log(`Installation directory: ${csDir}`) + } catch (error) { + console.error(`Error installing OVMS: ${error.message}`) + if (fs.existsSync(tempFilename)) { + fs.unlinkSync(tempFilename) + } + + // Check if ovmsDir is empty and remove it if so + try { + const ovmsDir = path.join(csDir, 'ovms') + const files = fs.readdirSync(ovmsDir) + if (files.length === 0) { + fs.rmSync(ovmsDir, { recursive: true }) + console.log(`Removed empty directory: ${ovmsDir}`) + } + } catch (cleanupError) { + console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`) + return 105 + } + + return 104 + } + + return 0 +} + +/** + * Get the CPU Name and ID + */ +function getCpuInfo() { + const cpuInfo = { + name: '', + id: '' + } + + // Use PowerShell to get CPU information + try { + const psCommand = `powershell -Command "Get-CimInstance -ClassName Win32_Processor | Select-Object Name, DeviceID | ConvertTo-Json"` + const psOutput = execSync(psCommand).toString() + const cpuData = JSON.parse(psOutput) + + if (Array.isArray(cpuData)) { + cpuInfo.name = cpuData[0].Name || '' + cpuInfo.id = cpuData[0].DeviceID || '' + } else { + cpuInfo.name = cpuData.Name || '' + cpuInfo.id = cpuData.DeviceID || '' + } + } catch (error) { + console.error(`Failed to get CPU info: ${error.message}`) + } + + return cpuInfo +} + +/** + * Main function to install OVMS + */ +async function installOvms() { + const platform = os.platform() + console.log(`Detected platform: ${platform}`) + + const cpuName = getCpuInfo().name + console.log(`CPU Name: ${cpuName}`) + + // Check if CPU name contains "Ultra" + if (!cpuName.toLowerCase().includes('intel') || !cpuName.toLowerCase().includes('ultra')) { + console.error('OVMS installation requires an Intel(R) Core(TM) Ultra CPU.') + return 101 + } + + // only support windows + if (platform !== 'win32') { + console.error('OVMS installation is only supported on Windows.') + return 102 + } + + return await downloadOvmsBinary() +} + +// Run the installation +installOvms() + .then((retcode) => { + if (retcode === 0) { + console.log('OVMS installation successful') + } else { + console.error('OVMS installation failed') + } + process.exit(retcode) + }) + .catch((error) => { + console.error('OVMS installation failed:', error) + process.exit(100) + }) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 0ef9cca2fa..275b3df4f9 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -35,6 +35,7 @@ import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ocrService } from './services/ocr/OcrService' +import OvmsManager from './services/OvmsManager' import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' import { FileServiceManager } from './services/remotefile/FileServiceManager' @@ -81,6 +82,7 @@ const obsidianVaultService = new ObsidianVaultService() const vertexAIService = VertexAIService.getInstance() const memoryService = MemoryService.getInstance() const dxtService = new DxtService() +const ovmsManager = new OvmsManager() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { const appUpdater = new AppUpdater() @@ -432,6 +434,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // system ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux')) ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname()) + ipcMain.handle(IpcChannel.System_GetCpuName, () => require('os').cpus()[0].model) ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => { const win = BrowserWindow.fromWebContents(e.sender) win && win.webContents.toggleDevTools() @@ -710,6 +713,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name)) ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js')) ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js')) + ipcMain.handle(IpcChannel.App_InstallOvmsBinary, () => runInstallScript('install-ovms.js')) //copilot ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage.bind(CopilotService)) @@ -841,6 +845,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ocrService.ocr(file, provider) ) + // OVMS + ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) => + ovmsManager.addModel(modelName, modelId, modelSource, task) + ) + ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel()) + ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels()) + ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms()) + ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus()) + ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms()) + ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms()) + // CherryAI ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params)) } diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts new file mode 100644 index 0000000000..f319200ac3 --- /dev/null +++ b/src/main/services/OvmsManager.ts @@ -0,0 +1,586 @@ +import { exec } from 'node:child_process' +import { homedir } from 'node:os' +import { promisify } from 'node:util' + +import { loggerService } from '@logger' +import * as fs from 'fs-extra' +import * as path from 'path' + +const logger = loggerService.withContext('OvmsManager') + +const execAsync = promisify(exec) + +interface OvmsProcess { + pid: number + path: string + workingDirectory: string +} + +interface ModelConfig { + name: string + base_path: string +} + +interface OvmsConfig { + mediapipe_config_list: ModelConfig[] +} + +class OvmsManager { + private ovms: OvmsProcess | null = null + + /** + * Recursively terminate a process and all its child processes + * @param pid Process ID to terminate + * @returns Promise<{ success: boolean; message?: string }> + */ + private async terminalProcess(pid: number): Promise<{ success: boolean; message?: string }> { + try { + // Check if the process is running + const processCheckCommand = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object Id | ConvertTo-Json` + const { stdout: processStdout } = await execAsync(`powershell -Command "${processCheckCommand}"`) + + if (!processStdout.trim()) { + logger.info(`Process with PID ${pid} is not running`) + return { success: true, message: `Process with PID ${pid} is not running` } + } + + // Find child processes + const childProcessCommand = `Get-WmiObject -Class Win32_Process | Where-Object { $_.ParentProcessId -eq ${pid} } | Select-Object ProcessId | ConvertTo-Json` + const { stdout: childStdout } = await execAsync(`powershell -Command "${childProcessCommand}"`) + + // If there are child processes, terminate them first + if (childStdout.trim()) { + const childProcesses = JSON.parse(childStdout) + const childList = Array.isArray(childProcesses) ? childProcesses : [childProcesses] + + logger.info(`Found ${childList.length} child processes for PID ${pid}`) + + // Recursively terminate each child process + for (const childProcess of childList) { + const childPid = childProcess.ProcessId + logger.info(`Terminating child process PID: ${childPid}`) + await this.terminalProcess(childPid) + } + } else { + logger.info(`No child processes found for PID ${pid}`) + } + + // Finally, terminate the parent process + const killCommand = `Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue` + await execAsync(`powershell -Command "${killCommand}"`) + logger.info(`Terminated process with PID: ${pid}`) + + // Wait for the process to disappear with 5-second timeout + const timeout = 5000 // 5 seconds + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const checkCommand = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object Id | ConvertTo-Json` + const { stdout: checkStdout } = await execAsync(`powershell -Command "${checkCommand}"`) + + if (!checkStdout.trim()) { + logger.info(`Process with PID ${pid} has disappeared`) + return { success: true, message: `Process ${pid} and all child processes terminated successfully` } + } + + // Wait 300ms before checking again + await new Promise((resolve) => setTimeout(resolve, 300)) + } + + logger.warn(`Process with PID ${pid} did not disappear within timeout`) + return { success: false, message: `Process ${pid} did not disappear within 5 seconds` } + } catch (error) { + logger.error(`Failed to terminate process ${pid}:`, error as Error) + return { success: false, message: `Failed to terminate process ${pid}` } + } + } + + /** + * Stop OVMS process if it's running + * @returns Promise<{ success: boolean; message?: string }> + */ + public async stopOvms(): Promise<{ success: boolean; message?: string }> { + try { + // Check if OVMS process is running + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('OVMS process is not running') + return { success: true, message: 'OVMS process is not running' } + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length === 0) { + logger.info('OVMS process is not running') + return { success: true, message: 'OVMS process is not running' } + } + + // Terminate all OVMS processes using terminalProcess + for (const process of processList) { + const result = await this.terminalProcess(process.Id) + if (!result.success) { + logger.error(`Failed to terminate OVMS process with PID: ${process.Id}, ${result.message}`) + return { success: false, message: `Failed to terminate OVMS process: ${result.message}` } + } + logger.info(`Terminated OVMS process with PID: ${process.Id}`) + } + + // Reset the ovms instance + this.ovms = null + + logger.info('OVMS process stopped successfully') + return { success: true, message: 'OVMS process stopped successfully' } + } catch (error) { + logger.error(`Failed to stop OVMS process: ${error}`) + return { success: false, message: 'Failed to stop OVMS process' } + } + } + + /** + * Run OVMS by ensuring config.json exists and executing run.bat + * @returns Promise<{ success: boolean; message?: string }> + */ + public async runOvms(): Promise<{ success: boolean; message?: string }> { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + const runBatPath = path.join(ovmsDir, 'run.bat') + + try { + // Check if config.json exists, if not create it with default content + if (!(await fs.pathExists(configPath))) { + logger.info(`Config file does not exist, creating: ${configPath}`) + + // Ensure the models directory exists + await fs.ensureDir(path.dirname(configPath)) + + // Create config.json with default content + const defaultConfig = { + mediapipe_config_list: [], + model_config_list: [] + } + + await fs.writeJson(configPath, defaultConfig, { spaces: 2 }) + logger.info(`Config file created: ${configPath}`) + } + + // Check if run.bat exists + if (!(await fs.pathExists(runBatPath))) { + logger.error(`run.bat not found at: ${runBatPath}`) + return { success: false, message: 'run.bat not found' } + } + + // Run run.bat without waiting for it to complete + logger.info(`Starting OVMS with run.bat: ${runBatPath}`) + exec(`"${runBatPath}"`, { cwd: ovmsDir }, (error) => { + if (error) { + logger.error(`Error running run.bat: ${error}`) + } + }) + + logger.info('OVMS started successfully') + return { success: true } + } catch (error) { + logger.error(`Failed to run OVMS: ${error}`) + return { success: false, message: 'Failed to run OVMS' } + } + } + + /** + * Get OVMS status - checks installation and running status + * @returns 'not-installed' | 'not-running' | 'running' + */ + public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { + const homeDir = homedir() + const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') + + try { + // Check if OVMS executable exists + if (!(await fs.pathExists(ovmsPath))) { + logger.info(`OVMS executable not found at: ${ovmsPath}`) + return 'not-installed' + } + + // Check if OVMS process is running + //const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq "${ovmsPath.replace(/\\/g, '\\\\')}" } | Select-Object Id | ConvertTo-Json`; + //const { stdout } = await execAsync(`powershell -Command "${psCommand}"`); + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('OVMS process not running') + return 'not-running' + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length > 0) { + logger.info('OVMS process is running') + return 'running' + } else { + logger.info('OVMS process not running') + return 'not-running' + } + } catch (error) { + logger.info(`Failed to check OVMS status: ${error}`) + return 'not-running' + } + } + + /** + * Initialize OVMS by finding the executable path and working directory + */ + public async initializeOvms(): Promise { + // Use PowerShell to find ovms.exe processes with their paths + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.error('Command to find OVMS process returned no output') + return false + } + logger.debug(`OVMS process output: ${stdout}`) + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + // Find the first process with a valid path + for (const process of processList) { + this.ovms = { + pid: process.Id, + path: process.Path, + workingDirectory: path.dirname(process.Path) + } + return true + } + + return this.ovms !== null + } + + /** + * Check if the Model Name and ID are valid, they are valid only if they are not used in the config.json + * @param modelName Name of the model to check + * @param modelId ID of the model to check + */ + public async isNameAndIDAvalid(modelName: string, modelId: string): Promise { + if (!modelName || !modelId) { + logger.error('Model name and ID cannot be empty') + return false + } + + const homeDir = homedir() + const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return false + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn(`No mediapipe_config_list found in config: ${configPath}`) + return false + } + + // Check if the model name or ID already exists in the config + const exists = config.mediapipe_config_list.some( + (model) => model.name === modelName || model.base_path === modelId + ) + if (exists) { + logger.warn(`Model with name "${modelName}" or ID "${modelId}" already exists in the config`) + return false + } + } catch (error) { + logger.error(`Failed to check model existence: ${error}`) + return false + } + + return true + } + + private async applyModelPath(modelDirPath: string): Promise { + const homeDir = homedir() + const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') + if (!(await fs.pathExists(patchDir))) { + return true + } + + const modelId = path.basename(modelDirPath) + + // get all sub directories in patchDir + const patchs = await fs.readdir(patchDir) + for (const patch of patchs) { + const fullPatchPath = path.join(patchDir, patch) + + if (fs.lstatSync(fullPatchPath).isDirectory()) { + if (modelId.toLowerCase().includes(patch.toLowerCase())) { + // copy all files from fullPath to modelDirPath + try { + const files = await fs.readdir(fullPatchPath) + for (const file of files) { + const srcFile = path.join(fullPatchPath, file) + const destFile = path.join(modelDirPath, file) + await fs.copyFile(srcFile, destFile) + } + } catch (error) { + logger.error(`Failed to copy files from ${fullPatchPath} to ${modelDirPath}: ${error}`) + return false + } + logger.info(`Applied patchs for model ${modelId}`) + return true + } + } + } + + return true + } + + /** + * Add a model to OVMS by downloading it + * @param modelName Name of the model to add + * @param modelId ID of the model to download + * @param modelSource Model Source: huggingface, hf-mirror and modelscope, default is huggingface + * @param task Task type: text_generation, embedding, rerank, image_generation + */ + public async addModel( + modelName: string, + modelId: string, + modelSource: string, + task: string = 'text_generation' + ): Promise<{ success: boolean; message?: string }> { + logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) + + const homeDir = homedir() + const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const pathModel = path.join(ovdndDir, 'models', modelId) + + try { + // check the ovdnDir+'models'+modelId exist or not + if (await fs.pathExists(pathModel)) { + logger.error(`Model with ID ${modelId} already exists`) + return { success: false, message: 'Model ID already exists!' } + } + + // remove the model directory if it exists + if (await fs.pathExists(pathModel)) { + logger.info(`Removing existing model directory: ${pathModel}`) + await fs.remove(pathModel) + } + + // Use ovdnd.exe for downloading instead of ovms.exe + const ovdndPath = path.join(ovdndDir, 'ovdnd.exe') + const command = + `"${ovdndPath}" --pull ` + + `--model_repository_path "${ovdndDir}/models" ` + + `--source_model "${modelId}" ` + + `--model_name "${modelName}" ` + + `--target_device GPU ` + + `--task ${task} ` + + `--overwrite_models` + + const env: Record = { + ...process.env, + OVMS_DIR: ovdndDir, + PYTHONHOME: path.join(ovdndDir, 'python'), + PATH: `${process.env.PATH};${ovdndDir};${path.join(ovdndDir, 'python')}` + } + + if (modelSource) { + env.HF_ENDPOINT = modelSource + } + + logger.info(`Running command: ${command} from ${modelSource}`) + const { stdout } = await execAsync(command, { env: env, cwd: ovdndDir }) + + logger.info('Model download completed') + logger.debug(`Command output: ${stdout}`) + } catch (error) { + // remove ovdnDir+'models'+modelId if it exists + if (await fs.pathExists(pathModel)) { + logger.info(`Removing failed model directory: ${pathModel}`) + await fs.remove(pathModel) + } + logger.error(`Failed to add model: ${error}`) + return { + success: false, + message: `Download model ${modelId} failed, please check following items and try it again:

- the model id

- network connection and proxy

` + } + } + + // Update config file + if (!(await this.updateModelConfig(modelName, modelId))) { + logger.error('Failed to update model config') + return { success: false, message: 'Failed to update model config' } + } + + if (!(await this.applyModelPath(pathModel))) { + logger.error('Failed to apply model patchs') + return { success: false, message: 'Failed to apply model patchs' } + } + + logger.info(`Model ${modelName} added successfully with ID ${modelId}`) + return { success: true } + } + + /** + * Stop the model download process if it's running + * @returns Promise<{ success: boolean; message?: string }> + */ + public async stopAddModel(): Promise<{ success: boolean; message?: string }> { + try { + // Check if ovdnd.exe process is running + const psCommand = `Get-Process -Name "ovdnd" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('ovdnd process is not running') + return { success: true, message: 'Model download process is not running' } + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length === 0) { + logger.info('ovdnd process is not running') + return { success: true, message: 'Model download process is not running' } + } + + // Terminate all ovdnd processes + for (const process of processList) { + this.terminalProcess(process.Id) + } + + logger.info('Model download process stopped successfully') + return { success: true, message: 'Model download process stopped successfully' } + } catch (error) { + logger.error(`Failed to stop model download process: ${error}`) + return { success: false, message: 'Failed to stop model download process' } + } + } + + /** + * check if the model id exists in the OVMS configuration + * @param modelId ID of the model to check + */ + public async checkModelExists(modelId: string): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return false + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn('No mediapipe_config_list found in config') + return false + } + + return config.mediapipe_config_list.some((model) => model.base_path === modelId) + } catch (error) { + logger.error(`Failed to check model existence: ${error}`) + return false + } + } + + /** + * Update the model configuration file + */ + public async updateModelConfig(modelName: string, modelId: string): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + // Ensure the models directory exists + await fs.ensureDir(path.dirname(configPath)) + let config: OvmsConfig + + // Read existing config or create new one + if (await fs.pathExists(configPath)) { + config = await fs.readJson(configPath) + } else { + config = { mediapipe_config_list: [] } + } + + // Ensure mediapipe_config_list exists + if (!config.mediapipe_config_list) { + config.mediapipe_config_list = [] + } + + // Add new model config + const newModelConfig: ModelConfig = { + name: modelName, + base_path: modelId + } + + // Check if model already exists, if so, update it + const existingIndex = config.mediapipe_config_list.findIndex((model) => model.base_path === modelId) + + if (existingIndex >= 0) { + config.mediapipe_config_list[existingIndex] = newModelConfig + logger.info(`Updated existing model config: ${modelName}`) + } else { + config.mediapipe_config_list.push(newModelConfig) + logger.info(`Added new model config: ${modelName}`) + } + + // Write config back to file + await fs.writeJson(configPath, config, { spaces: 2 }) + logger.info(`Config file updated: ${configPath}`) + } catch (error) { + logger.error(`Failed to update model config: ${error}`) + return false + } + return true + } + + /** + * Get all models from OVMS config, filtered for image generation models + * @returns Array of model configurations + */ + public async getModels(): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return [] + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn('No mediapipe_config_list found in config') + return [] + } + + // Filter models for image generation (SD, Stable-Diffusion, Stable Diffusion, FLUX) + const imageGenerationModels = config.mediapipe_config_list.filter((model) => { + const modelName = model.name.toLowerCase() + return ( + modelName.startsWith('sd') || + modelName.startsWith('stable-diffusion') || + modelName.startsWith('stable diffusion') || + modelName.startsWith('flux') + ) + }) + + logger.info(`Found ${imageGenerationModels.length} image generation models`) + return imageGenerationModels + } catch (error) { + logger.error(`Failed to get models: ${error}`) + return [] + } + } +} + +export default OvmsManager diff --git a/src/preload/index.ts b/src/preload/index.ts index faa0335aeb..9244076a9a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -95,7 +95,8 @@ const api = { }, system: { getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType), - getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname) + getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname), + getCpuName: () => ipcRenderer.invoke(IpcChannel.System_GetCpuName) }, devTools: { toggle: () => ipcRenderer.invoke(IpcChannel.System_ToggleDevTools) @@ -285,6 +286,16 @@ const api = { clearAuthCache: (projectId: string, clientEmail?: string) => ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail) }, + ovms: { + addModel: (modelName: string, modelId: string, modelSource: string, task: string) => + ipcRenderer.invoke(IpcChannel.Ovms_AddModel, modelName, modelId, modelSource, task), + stopAddModel: () => ipcRenderer.invoke(IpcChannel.Ovms_StopAddModel), + getModels: () => ipcRenderer.invoke(IpcChannel.Ovms_GetModels), + isRunning: () => ipcRenderer.invoke(IpcChannel.Ovms_IsRunning), + getStatus: () => ipcRenderer.invoke(IpcChannel.Ovms_GetStatus), + runOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_RunOVMS), + stopOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_StopOVMS) + }, config: { set: (key: string, value: any, isNotify: boolean = false) => ipcRenderer.invoke(IpcChannel.Config_Set, key, value, isNotify), @@ -350,6 +361,7 @@ const api = { getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name), installUVBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallUvBinary), installBunBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallBunBinary), + installOvmsBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallOvmsBinary), protocol: { onReceiveData: (callback: (data: { url: string; params: any }) => void) => { const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => { diff --git a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts index f7b4a80f46..e7194c240b 100644 --- a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts +++ b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts @@ -12,6 +12,7 @@ import { VertexAPIClient } from './gemini/VertexAPIClient' import { NewAPIClient } from './newapi/NewAPIClient' import { OpenAIAPIClient } from './openai/OpenAIApiClient' import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient' +import { OVMSClient } from './ovms/OVMSClient' import { PPIOAPIClient } from './ppio/PPIOAPIClient' import { ZhipuAPIClient } from './zhipu/ZhipuAPIClient' @@ -63,6 +64,12 @@ export class ApiClientFactory { return instance } + if (provider.id === 'ovms') { + logger.debug(`Creating OVMSClient for provider: ${provider.id}`) + instance = new OVMSClient(provider) as BaseApiClient + return instance + } + // 然后检查标准的 Provider Type switch (provider.type) { case 'openai': diff --git a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts new file mode 100644 index 0000000000..d1fd4ed8ba --- /dev/null +++ b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts @@ -0,0 +1,56 @@ +import { loggerService } from '@logger' +import { isSupportedModel } from '@renderer/config/models' +import { objectKeys, Provider } from '@renderer/types' +import OpenAI from 'openai' + +import { OpenAIAPIClient } from '../openai/OpenAIApiClient' + +const logger = loggerService.withContext('OVMSClient') + +export class OVMSClient extends OpenAIAPIClient { + constructor(provider: Provider) { + super(provider) + } + + override async listModels(): Promise { + try { + const sdk = await this.getSdkInstance() + + const chatModelsResponse = await sdk.request({ + method: 'get', + path: '../v1/config' + }) + logger.debug(`Chat models response: ${JSON.stringify(chatModelsResponse)}`) + + // Parse the config response to extract model information + const config = chatModelsResponse as Record + const models = objectKeys(config) + .map((modelName) => { + const modelInfo = config[modelName] + + // Check if model has at least one version with "AVAILABLE" state + const hasAvailableVersion = modelInfo?.model_version_status?.some( + (versionStatus: any) => versionStatus?.state === 'AVAILABLE' + ) + + if (hasAvailableVersion) { + return { + id: modelName, + object: 'model' as const, + owned_by: 'ovms', + created: Date.now() + } + } + return null // Skip models without available versions + }) + .filter(Boolean) // Remove null entries + logger.debug(`Processed models: ${JSON.stringify(models)}`) + + // Filter out unsupported models + return models.filter((model): model is OpenAI.Models.Model => model !== null && isSupportedModel(model)) + } catch (error) { + logger.error(`Error listing OVMS models: ${error}`) + return [] + } + } +} diff --git a/src/renderer/src/assets/images/providers/intel.png b/src/renderer/src/assets/images/providers/intel.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d9e19d745763bc00c7337fa89605762c2778d3 GIT binary patch literal 3706 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 = { id: 'deepseek-r1', name: 'DeepSeek-R1', provider: 'burncloud', group: 'deepseek-ai' }, { id: 'deepseek-v3', name: 'DeepSeek-V3', provider: 'burncloud', group: 'deepseek-ai' } ], + ovms: [], ollama: [], lmstudio: [], silicon: [ diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 64e78e847a..ddaac53342 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -24,6 +24,7 @@ import GrokProviderLogo from '@renderer/assets/images/providers/grok.png' import GroqProviderLogo from '@renderer/assets/images/providers/groq.png' import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png' import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png' +import IntelOvmsLogo from '@renderer/assets/images/providers/intel.png' import JinaProviderLogo from '@renderer/assets/images/providers/jina.png' import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png' @@ -109,6 +110,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = isSystem: true, enabled: false }, + ovms: { + id: 'ovms', + name: 'OpenVINO Model Server', + type: 'openai', + apiKey: '', + apiHost: 'http://localhost:8000/v3/', + models: SYSTEM_MODELS.ovms, + isSystem: true, + enabled: false + }, ocoolai: { id: 'ocoolai', name: 'ocoolAI', @@ -649,6 +660,7 @@ export const PROVIDER_LOGO_MAP: AtLeast = { yi: ZeroOneProviderLogo, groq: GroqProviderLogo, zhipu: ZhipuProviderLogo, + ovms: IntelOvmsLogo, ollama: OllamaProviderLogo, lmstudio: LMStudioProviderLogo, moonshot: MoonshotProviderLogo, @@ -1034,6 +1046,16 @@ export const PROVIDER_URLS: Record = { models: 'https://console.groq.com/docs/models' } }, + ovms: { + api: { + url: 'http://localhost:8000/v3/' + }, + websites: { + official: 'https://www.intel.com/content/www/us/en/developer/tools/openvino-toolkit/overview.html', + docs: 'https://docs.openvino.ai/2025/model-server/ovms_what_is_openvino_model_server.html', + models: 'https://www.modelscope.cn/organization/OpenVINO' + } + }, ollama: { api: { url: 'http://localhost:11434' diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index a07daa975f..ad3c326d9f 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -61,6 +61,7 @@ const providerKeyMap = { nvidia: 'provider.nvidia', o3: 'provider.o3', ocoolai: 'provider.ocoolai', + ovms: 'provider.ovms', ollama: 'provider.ollama', openai: 'provider.openai', openrouter: 'provider.openrouter', diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index dfd09049ed..94abc7667d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -251,6 +251,7 @@ "added": "Added", "case_sensitive": "Case Sensitive", "collapse": "Collapse", + "download": "Download", "includes_user_questions": "Include Your Questions", "manage": "Manage", "select_model": "Select Model", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Install", + "installing": "Installing", + "reinstall": "Re-Install", + "run": "Run OVMS", + "starting": "Starting", + "stop": "Stop OVMS", + "stopping": "Stopping" + }, + "description": "

1. Download OV Models.

2. Add Models in 'Manager'.

Support Windows Only!

OVMS Install Path: '%USERPROFILE%\\.cherrystudio\\ovms' .

Please refer to Intel OVMS Guide

", + "download": { + "button": "Download", + "error": "Download Error", + "model_id": { + "label": "Model ID:", + "model_id_pattern": "Model ID must start with OpenVINO/", + "placeholder": "Required e.g. OpenVINO/Qwen3-8B-int4-ov", + "required": "Please enter the model ID" + }, + "model_name": { + "label": "Model Name:", + "placeholder": "Required e.g. Qwen3-8B-int4-ov", + "required": "Please enter the model name" + }, + "model_source": "Model Source:", + "model_task": "Model Task:", + "success": "Download successful", + "success_desc": "Model \"{{modelName}}\"-\"{{modelId}}\" downloaded successfully, please go to the OVMS management interface to add the model", + "tip": "The model is downloading, sometimes it takes hours. Please be patient...", + "title": "Download Intel OpenVINO Model" + }, + "failed": { + "install": "Install OVMS failed:", + "install_code_100": "Unknown Error", + "install_code_101": "Only supports Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Only supports Windows", + "install_code_103": "Download OVMS runtime failed", + "install_code_104": "Uncompress OVMS runtime failed", + "install_code_105": "Clean OVMS runtime failed", + "run": "Run OVMS failed:", + "stop": "Stop OVMS failed:" + }, + "status": { + "not_installed": "OVMS is not installed", + "not_running": "OVMS is not running", + "running": "OVMS is running", + "unknown": "OVMS status unknown" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Aspect Ratio", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index dd3fa37be1..c09c94cbbf 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -251,6 +251,7 @@ "added": "已添加", "case_sensitive": "区分大小写", "collapse": "收起", + "download": "下载", "includes_user_questions": "包含用户提问", "manage": "管理", "select_model": "选择模型", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "安装", + "installing": "正在安装", + "reinstall": "重装", + "run": "运行 OVMS", + "starting": "启动中", + "stop": "停止 OVMS", + "stopping": "停止中" + }, + "description": "

1. 下载 OV 模型.

2. 在 'Manager' 中添加模型.

仅支持 Windows!

OVMS 安装路径: '%USERPROFILE%\\.cherrystudio\\ovms' .

请参考 Intel OVMS 指南

", + "download": { + "button": "下载", + "error": "选择失败", + "model_id": { + "label": "模型 ID", + "model_id_pattern": "模型 ID 必须以 OpenVINO/ 开头", + "placeholder": "必填,例如 OpenVINO/Qwen3-8B-int4-ov", + "required": "请输入模型 ID" + }, + "model_name": { + "label": "模型名称", + "placeholder": "必填,例如 Qwen3-8B-int4-ov", + "required": "请输入模型名称" + }, + "model_source": "模型来源:", + "model_task": "模型任务:", + "success": "下载成功", + "success_desc": "模型\"{{modelName}}\"-\"{{modelId}}\"下载成功,请前往 OVMS 管理界面添加模型", + "tip": "模型正在下载,有时需要几个小时。请耐心等待...", + "title": "下载 Intel OpenVINO 模型" + }, + "failed": { + "install": "安装 OVMS 失败:", + "install_code_100": "未知错误", + "install_code_101": "仅支持 Intel(R) Core(TM) Ultra CPU", + "install_code_102": "仅支持 Windows", + "install_code_103": "下载 OVMS runtime 失败", + "install_code_104": "解压 OVMS runtime 失败", + "install_code_105": "清理 OVMS runtime 失败", + "run": "运行 OVMS 失败:", + "stop": "停止 OVMS 失败:" + }, + "status": { + "not_installed": "OVMS 未安装", + "not_running": "OVMS 未运行", + "running": "OVMS 正在运行", + "unknown": "OVMS 状态未知" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "画幅比例", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8 大模型开放平台", "poe": "Poe", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4e162abda3..a1aa1cc7fa 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -251,6 +251,7 @@ "added": "已新增", "case_sensitive": "區分大小寫", "collapse": "折疊", + "download": "下載", "includes_user_questions": "包含使用者提問", "manage": "管理", "select_model": "選擇模型", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "安裝", + "installing": "正在安裝", + "reinstall": "重新安裝", + "run": "執行 OVMS", + "starting": "啟動中", + "stop": "停止 OVMS", + "stopping": "停止中" + }, + "description": "

1. 下載 OV 模型。

2. 在 'Manager' 中新增模型。

僅支援 Windows!

OVMS 安裝路徑: '%USERPROFILE%\\.cherrystudio\\ovms' 。

請參考 Intel OVMS 指南

", + "download": { + "button": "下載", + "error": "選擇失敗", + "model_id": { + "label": "模型 ID", + "model_id_pattern": "模型 ID 必須以 OpenVINO/ 開頭", + "placeholder": "必填,例如 OpenVINO/Qwen3-8B-int4-ov", + "required": "請輸入模型 ID" + }, + "model_name": { + "label": "模型名稱", + "placeholder": "必填,例如 Qwen3-8B-int4-ov", + "required": "請輸入模型名稱" + }, + "model_source": "模型來源:", + "model_task": "模型任務:", + "success": "下載成功", + "success_desc": "模型\"{{modelName}}\"-\"{{modelId}}\"下載成功,請前往 OVMS 管理界面添加模型", + "tip": "模型正在下載,有時需要幾個小時。請耐心等候...", + "title": "下載 Intel OpenVINO 模型" + }, + "failed": { + "install": "安裝 OVMS 失敗:", + "install_code_100": "未知錯誤", + "install_code_101": "僅支援 Intel(R) Core(TM) Ultra CPU", + "install_code_102": "僅支援 Windows", + "install_code_103": "下載 OVMS runtime 失敗", + "install_code_104": "解壓 OVMS runtime 失敗", + "install_code_105": "清理 OVMS runtime 失敗", + "run": "執行 OVMS 失敗:", + "stop": "停止 OVMS 失敗:" + }, + "status": { + "not_installed": "OVMS 未安裝", + "not_running": "OVMS 未執行", + "running": "OVMS 正在執行", + "unknown": "OVMS 狀態未知" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "畫幅比例", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8 大模型開放平台", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 71aca53b82..a071f27783 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -251,6 +251,7 @@ "added": "προστέθηκε", "case_sensitive": "Διάκριση πεζών/κεφαλαίων", "collapse": "συμπεριλάβετε", + "download": "Λήψη", "includes_user_questions": "Περιλαμβάνει ερωτήσεις χρήστη", "manage": "χειριστείτε", "select_model": "επιλογή μοντέλου", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Εγκατάσταση", + "installing": "Εγκατάσταση σε εξέλιξη", + "reinstall": "Επανεγκατάσταση", + "run": "Εκτέλεση OVMS", + "starting": "Εκκίνηση σε εξέλιξη", + "stop": "Διακοπή OVMS", + "stopping": "Διακοπή σε εξέλιξη" + }, + "description": "

1. Λήψη μοντέλου OV.

2. Προσθήκη μοντέλου στο 'Manager'.

Υποστηρίζεται μόνο στα Windows!

Διαδρομή εγκατάστασης OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Ανατρέξτε στον Οδηγό Intel OVMS

", + "download": { + "button": "Λήψη", + "error": "Η επιλογή απέτυχε", + "model_id": { + "label": "Αναγνωριστικό μοντέλου:", + "model_id_pattern": "Το αναγνωριστικό μοντέλου πρέπει να ξεκινά με OpenVINO/", + "placeholder": "Απαιτείται, π.χ. OpenVINO/Qwen3-8B-int4-ov", + "required": "Παρακαλώ εισάγετε το αναγνωριστικό μοντέλου" + }, + "model_name": { + "label": "Όνομα μοντέλου:", + "placeholder": "Απαιτείται, π.χ. Qwen3-8B-int4-ov", + "required": "Παρακαλώ εισάγετε το όνομα του μοντέλου" + }, + "model_source": "Πηγή μοντέλου:", + "model_task": "Εργασία μοντέλου:", + "success": "Η λήψη ολοκληρώθηκε με επιτυχία", + "success_desc": "Το μοντέλο \"{{modelName}}\"-\"{{modelId}}\" λήφθηκε επιτυχώς, παρακαλώ μεταβείτε στη διεπαφή διαχείρισης OVMS για να προσθέσετε το μοντέλο", + "tip": "Το μοντέλο κατεβαίνει, μερικές φορές χρειάζονται αρκετές ώρες. Παρακαλώ περιμένετε υπομονετικά...", + "title": "Λήψη μοντέλου Intel OpenVINO" + }, + "failed": { + "install": "Η εγκατάσταση του OVMS απέτυχε:", + "install_code_100": "Άγνωστο σφάλμα", + "install_code_101": "Υποστηρίζεται μόνο σε Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Υποστηρίζεται μόνο στα Windows", + "install_code_103": "Η λήψη του OVMS runtime απέτυχε", + "install_code_104": "Η αποσυμπίεση του OVMS runtime απέτυχε", + "install_code_105": "Ο καθαρισμός του OVMS runtime απέτυχε", + "run": "Η εκτέλεση του OVMS απέτυχε:", + "stop": "Η διακοπή του OVMS απέτυχε:" + }, + "status": { + "not_installed": "Το OVMS δεν έχει εγκατασταθεί", + "not_running": "Το OVMS δεν εκτελείται", + "running": "Το OVMS εκτελείται", + "unknown": "Άγνωστη κατάσταση OVMS" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Λόγος διαστάσεων", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "Πλατφόρμα Ανοιχτής Μεγάλης Μοντέλου PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index de3b5cb17f..7ae0a1bd3c 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -251,6 +251,7 @@ "added": "Agregado", "case_sensitive": "Distingue mayúsculas y minúsculas", "collapse": "Colapsar", + "download": "Descargar", "includes_user_questions": "Incluye preguntas del usuario", "manage": "Administrar", "select_model": "Seleccionar Modelo", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Instalar", + "installing": "Instalando", + "reinstall": "Reinstalar", + "run": "Ejecutar OVMS", + "starting": "Iniciando", + "stop": "Detener OVMS", + "stopping": "Deteniendo" + }, + "description": "

1. Descargar modelo OV.

2. Agregar modelo en 'Administrador'.

¡Solo compatible con Windows!

Ruta de instalación de OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Consulte la Guía de Intel OVMS

", + "download": { + "button": "Descargar", + "error": "Selección fallida", + "model_id": { + "label": "ID del modelo:", + "model_id_pattern": "El ID del modelo debe comenzar con OpenVINO/", + "placeholder": "Requerido, por ejemplo, OpenVINO/Qwen3-8B-int4-ov", + "required": "Por favor, ingrese el ID del modelo" + }, + "model_name": { + "label": "Nombre del modelo:", + "placeholder": "Requerido, por ejemplo, Qwen3-8B-int4-ov", + "required": "Por favor, ingrese el nombre del modelo" + }, + "model_source": "Fuente del modelo:", + "model_task": "Tarea del modelo:", + "success": "Descarga exitosa", + "success_desc": "El modelo \"{{modelName}}\"-\"{{modelId}}\" se descargó exitosamente, por favor vaya a la interfaz de administración de OVMS para agregar el modelo", + "tip": "El modelo se está descargando, a veces toma varias horas. Por favor espere pacientemente...", + "title": "Descargar modelo Intel OpenVINO" + }, + "failed": { + "install": "Error al instalar OVMS:", + "install_code_100": "Error desconocido", + "install_code_101": "Solo compatible con CPU Intel(R) Core(TM) Ultra", + "install_code_102": "Solo compatible con Windows", + "install_code_103": "Error al descargar el tiempo de ejecución de OVMS", + "install_code_104": "Error al descomprimir el tiempo de ejecución de OVMS", + "install_code_105": "Error al limpiar el tiempo de ejecución de OVMS", + "run": "Error al ejecutar OVMS:", + "stop": "Error al detener OVMS:" + }, + "status": { + "not_installed": "OVMS no instalado", + "not_running": "OVMS no está en ejecución", + "running": "OVMS en ejecución", + "unknown": "Estado de OVMS desconocido" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Relación de aspecto", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplejidad", "ph8": "Plataforma Abierta de Grandes Modelos PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 66a3a5b4ae..64a788f266 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -251,6 +251,7 @@ "added": "Ajouté", "case_sensitive": "Respecter la casse", "collapse": "Réduire", + "download": "Télécharger", "includes_user_questions": "Inclure les questions de l'utilisateur", "manage": "Gérer", "select_model": "Sélectionner le Modèle", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Installer", + "installing": "Installation en cours", + "reinstall": "Réinstaller", + "run": "Exécuter OVMS", + "starting": "Démarrage en cours", + "stop": "Arrêter OVMS", + "stopping": "Arrêt en cours" + }, + "description": "

1. Télécharger le modèle OV.

2. Ajouter le modèle dans 'Manager'.

Uniquement compatible avec Windows !

Chemin d'installation d'OVMS : '%USERPROFILE%\\.cherrystudio\\ovms' .

Veuillez vous référer au Guide Intel OVMS

", + "download": { + "button": "Télécharger", + "error": "Échec de la sélection", + "model_id": { + "label": "ID du modèle :", + "model_id_pattern": "L'ID du modèle doit commencer par OpenVINO/", + "placeholder": "Requis, par exemple OpenVINO/Qwen3-8B-int4-ov", + "required": "Veuillez saisir l'ID du modèle" + }, + "model_name": { + "label": "Nom du modèle :", + "placeholder": "Requis, par exemple Qwen3-8B-int4-ov", + "required": "Veuillez saisir le nom du modèle" + }, + "model_source": "Source du modèle :", + "model_task": "Tâche du modèle :", + "success": "Téléchargement réussi", + "success_desc": "Le modèle \"{{modelName}}\"-\"{{modelId}}\" a été téléchargé avec succès, veuillez vous rendre à l'interface de gestion OVMS pour ajouter le modèle", + "tip": "Le modèle est en cours de téléchargement, cela peut parfois prendre plusieurs heures. Veuillez patienter...", + "title": "Télécharger le modèle Intel OpenVINO" + }, + "failed": { + "install": "Échec de l'installation d'OVMS :", + "install_code_100": "Erreur inconnue", + "install_code_101": "Uniquement compatible avec les processeurs Intel(R) Core(TM) Ultra", + "install_code_102": "Uniquement compatible avec Windows", + "install_code_103": "Échec du téléchargement du runtime OVMS", + "install_code_104": "Échec de la décompression du runtime OVMS", + "install_code_105": "Échec du nettoyage du runtime OVMS", + "run": "Échec de l'exécution d'OVMS :", + "stop": "Échec de l'arrêt d'OVMS :" + }, + "status": { + "not_installed": "OVMS non installé", + "not_running": "OVMS n'est pas en cours d'exécution", + "running": "OVMS en cours d'exécution", + "unknown": "État d'OVMS inconnu" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Format d'image", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexité", "ph8": "Plateforme ouverte de grands modèles PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index bee5f54470..23eea05fe6 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -251,6 +251,7 @@ "added": "追加済み", "case_sensitive": "大文字と小文字の区別", "collapse": "折りたたむ", + "download": "ダウンロード", "includes_user_questions": "ユーザーからの質問を含む", "manage": "管理", "select_model": "モデルを選択", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "インストール", + "installing": "インストール中", + "reinstall": "再インストール", + "run": "OVMSを実行", + "starting": "起動中", + "stop": "OVMSを停止", + "stopping": "停止中" + }, + "description": "

1. OVモデルをダウンロードします。

2. 'マネージャー'でモデルを追加します。

Windowsのみサポート!

OVMSインストールパス: '%USERPROFILE%\\.cherrystudio\\ovms' 。

詳細はIntel OVMSガイドをご参照ください。

", + "download": { + "button": "ダウンロード", + "error": "ダウンロードエラー", + "model_id": { + "label": "モデルID", + "model_id_pattern": "モデルIDはOpenVINO/で始まる必要があります", + "placeholder": "必須 例: OpenVINO/Qwen3-8B-int4-ov", + "required": "モデルIDを入力してください" + }, + "model_name": { + "label": "モデル名", + "placeholder": "必須 例: Qwen3-8B-int4-ov", + "required": "モデル名を入力してください" + }, + "model_source": "モデルソース:", + "model_task": "モデルタスク:", + "success": "ダウンロード成功", + "success_desc": "モデル\"{{modelName}}\"-\"{{modelId}}\"ダウンロード成功、OVMS管理インターフェースに移動してモデルを追加してください", + "tip": "モデルはダウンロードされていますが、時には数時間かかります。我慢してください...", + "title": "Intel OpenVINOモデルをダウンロード" + }, + "failed": { + "install": "OVMSのインストールに失敗しました:", + "install_code_100": "不明なエラー", + "install_code_101": "Intel(R) Core(TM) Ultra CPUのみサポート", + "install_code_102": "Windowsのみサポート", + "install_code_103": "OVMSランタイムのダウンロードに失敗しました", + "install_code_104": "OVMSランタイムの解凍に失敗しました", + "install_code_105": "OVMSランタイムのクリーンアップに失敗しました", + "run": "OVMSの実行に失敗しました:", + "stop": "OVMSの停止に失敗しました:" + }, + "status": { + "not_installed": "OVMSはインストールされていません", + "not_running": "OVMSは実行されていません", + "running": "OVMSは実行中です", + "unknown": "OVMSのステータスが不明です" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "画幅比例", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 5c9bbf566e..ab9bec0e66 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -251,6 +251,7 @@ "added": "Adicionado", "case_sensitive": "Diferenciar maiúsculas e minúsculas", "collapse": "Recolher", + "download": "Baixar", "includes_user_questions": "Incluir perguntas do usuário", "manage": "Gerenciar", "select_model": "Selecionar Modelo", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Instalar", + "installing": "Instalando", + "reinstall": "Reinstalar", + "run": "Executar OVMS", + "starting": "Iniciando", + "stop": "Parar OVMS", + "stopping": "Parando" + }, + "description": "

1. Baixe o modelo OV.

2. Adicione o modelo no 'Gerenciador'.

Compatível apenas com Windows!

Caminho de instalação do OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Consulte o Guia do Intel OVMS

", + "download": { + "button": "Baixar", + "error": "Falha na seleção", + "model_id": { + "label": "ID do modelo:", + "model_id_pattern": "O ID do modelo deve começar com OpenVINO/", + "placeholder": "Obrigatório, por exemplo, OpenVINO/Qwen3-8B-int4-ov", + "required": "Por favor, insira o ID do modelo" + }, + "model_name": { + "label": "Nome do modelo:", + "placeholder": "Obrigatório, por exemplo, Qwen3-8B-int4-ov", + "required": "Por favor, insira o nome do modelo" + }, + "model_source": "Fonte do modelo:", + "model_task": "Tarefa do modelo:", + "success": "Download concluído com sucesso", + "success_desc": "O modelo \"{{modelName}}\"-\"{{modelId}}\" foi baixado com sucesso, por favor vá para a interface de gerenciamento OVMS para adicionar o modelo", + "tip": "O modelo está sendo baixado, às vezes leva várias horas. Por favor aguarde pacientemente...", + "title": "Baixar modelo Intel OpenVINO" + }, + "failed": { + "install": "Falha na instalação do OVMS:", + "install_code_100": "Erro desconhecido", + "install_code_101": "Compatível apenas com CPU Intel(R) Core(TM) Ultra", + "install_code_102": "Compatível apenas com Windows", + "install_code_103": "Falha ao baixar o tempo de execução do OVMS", + "install_code_104": "Falha ao descompactar o tempo de execução do OVMS", + "install_code_105": "Falha ao limpar o tempo de execução do OVMS", + "run": "Falha ao executar o OVMS:", + "stop": "Falha ao parar o OVMS:" + }, + "status": { + "not_installed": "OVMS não instalado", + "not_running": "OVMS não está em execução", + "running": "OVMS em execução", + "unknown": "Status do OVMS desconhecido" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Proporção da Imagem", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexidade", "ph8": "Plataforma Aberta de Grandes Modelos PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index ec50afccd0..ccc1f49344 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -251,6 +251,7 @@ "added": "Добавлено", "case_sensitive": "Чувствительность к регистру", "collapse": "Свернуть", + "download": "Скачать", "includes_user_questions": "Включает вопросы пользователей", "manage": "Редактировать", "select_model": "Выбрать модель", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Установить", + "installing": "Установка", + "reinstall": "Переустановить", + "run": "Запустить OVMS", + "starting": "Запуск", + "stop": "Остановить OVMS", + "stopping": "Остановка" + }, + "description": "

1. Загрузите модели OV.

2. Добавьте модели в 'Менеджер'.

Поддерживается только Windows!

Путь установки OVMS: '%USERPROFILE%\\.cherrystudio\\ovms'.

Пожалуйста, ознакомьтесь с руководством Intel OVMS

", + "download": { + "button": "Скачать", + "error": "Ошибка загрузки", + "model_id": { + "label": "ID модели", + "model_id_pattern": "ID модели должен начинаться с OpenVINO/", + "placeholder": "Обязательно, например: OpenVINO/Qwen3-8B-int4-ov", + "required": "Пожалуйста, введите ID модели" + }, + "model_name": { + "label": "Название модели:", + "placeholder": "Обязательно, например: Qwen3-8B-int4-ov", + "required": "Пожалуйста, введите название модели" + }, + "model_source": "Источник модели:", + "model_task": "Задача модели:", + "success": "Скачивание успешно", + "success_desc": "Модель \"{{modelName}}\"-\"{{modelId}}\" успешно скачана, пожалуйста, перейдите в интерфейс управления OVMS, чтобы добавить модель", + "tip": "Модель загружается, иногда это занимает часы. Пожалуйста, будьте терпеливы...", + "title": "Скачать модель Intel OpenVINO" + }, + "failed": { + "install": "Ошибка установки OVMS:", + "install_code_100": "Неизвестная ошибка", + "install_code_101": "Поддерживаются только процессоры Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Поддерживается только Windows", + "install_code_103": "Ошибка загрузки среды выполнения OVMS", + "install_code_104": "Ошибка распаковки среды выполнения OVMS", + "install_code_105": "Ошибка очистки среды выполнения OVMS", + "run": "Ошибка запуска OVMS:", + "stop": "Ошибка остановки OVMS:" + }, + "status": { + "not_installed": "OVMS не установлен", + "not_running": "OVMS не запущен", + "running": "OVMS запущен", + "unknown": "Статус OVMS неизвестен" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Пропорции изображения", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx new file mode 100644 index 0000000000..0efe081a20 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx @@ -0,0 +1,353 @@ +import { loggerService } from '@logger' +import { TopView } from '@renderer/components/TopView' +import { Provider } from '@renderer/types' +import { AutoComplete, Button, Flex, Form, FormProps, Input, Modal, Progress, Select } from 'antd' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useTimer } from '../../../../hooks/useTimer' + +const logger = loggerService.withContext('OVMSClient') + +interface ShowParams { + title: string + provider: Provider +} + +interface Props extends ShowParams { + resolve: (data: any) => unknown +} + +type FieldType = { + modelName: string + modelId: string + modelSource: string + task: string +} + +interface PresetModel { + modelId: string + modelName: string + modelSource: string + task: string + label: string +} + +const PRESET_MODELS: PresetModel[] = [ + { + modelId: 'OpenVINO/Qwen3-8B-int4-ov', + modelName: 'Qwen3-8B-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'text_generation', + label: 'Qwen3-8B-int4-ov (Text Generation)' + }, + { + modelId: 'OpenVINO/bge-base-en-v1.5-fp16-ov', + modelName: 'bge-base-en-v1.5-fp16-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'embeddings', + label: 'bge-base-en-v1.5-fp16-ov (Embeddings)' + }, + { + modelId: 'OpenVINO/bge-reranker-base-fp16-ov', + modelName: 'bge-reranker-base-fp16-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'rerank', + label: 'bge-reranker-base-fp16-ov (Rerank)' + }, + { + modelId: 'OpenVINO/DeepSeek-R1-Distill-Qwen-7B-int4-ov', + modelName: 'DeepSeek-R1-Distill-Qwen-7B-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'text_generation', + label: 'DeepSeek-R1-Distill-Qwen-7B-int4-ov (Text Generation)' + }, + { + modelId: 'OpenVINO/stable-diffusion-v1-5-int8-ov', + modelName: 'stable-diffusion-v1-5-int8-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'image_generation', + label: 'stable-diffusion-v1-5-int8-ov (Image Generation)' + }, + { + modelId: 'OpenVINO/FLUX.1-schnell-int4-ov', + modelName: 'FLUX.1-schnell-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'image_generation', + label: 'FLUX.1-schnell-int4-ov (Image Generation)' + } +] + +const PopupContainer: React.FC = ({ title, resolve }) => { + const [open, setOpen] = useState(true) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState(0) + const [cancelled, setCancelled] = useState(false) + const [form] = Form.useForm() + const { t } = useTranslation() + const { setIntervalTimer, clearIntervalTimer, setTimeoutTimer } = useTimer() + + const startFakeProgress = () => { + setProgress(0) + setIntervalTimer( + 'progress', + () => { + setProgress((prev) => { + if (prev >= 95) { + return prev // Stop at 95% until actual completion + } + // Simulate realistic download progress with slowing speed + const increment = + prev < 30 + ? Math.random() * 1 + 0.25 + : prev < 60 + ? Math.random() * 0.5 + 0.125 + : Math.random() * 0.25 + 0.03125 + + return Math.min(prev + increment, 95) + }) + }, + 500 + ) + } + + const stopFakeProgress = (complete = false) => { + clearIntervalTimer('progress') + if (complete) { + setProgress(100) + // Reset progress after a short delay + setTimeoutTimer('progress-reset', () => setProgress(0), 1500) + } else { + setProgress(0) + } + } + + const handlePresetSelect = (value: string) => { + const selectedPreset = PRESET_MODELS.find((model) => model.modelId === value) + if (selectedPreset) { + form.setFieldsValue({ + modelId: selectedPreset.modelId, + modelName: selectedPreset.modelName, + modelSource: selectedPreset.modelSource, + task: selectedPreset.task + }) + } + } + + const handleModelIdChange = (value: string) => { + if (value) { + // Extract model name from model ID (part after last '/') + const lastSlashIndex = value.lastIndexOf('/') + if (lastSlashIndex !== -1 && lastSlashIndex < value.length - 1) { + const modelName = value.substring(lastSlashIndex + 1) + form.setFieldValue('modelName', modelName) + } + } + } + + const onCancel = async () => { + if (loading) { + // Stop the download + try { + setCancelled(true) // Mark as cancelled by user + logger.info('Stopping download...') + await window.api.ovms.stopAddModel() + stopFakeProgress(false) + setLoading(false) + } catch (error) { + logger.error(`Failed to stop download: ${error}`) + } + return + } + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + const onFinish: FormProps['onFinish'] = async (values) => { + setLoading(true) + setCancelled(false) // Reset cancelled state + startFakeProgress() + try { + const { modelName, modelId, modelSource, task } = values + logger.info(`🔄 Downloading model: ${modelName} with ID: ${modelId}, source: ${modelSource}, task: ${task}`) + const result = await window.api.ovms.addModel(modelName, modelId, modelSource, task) + + if (result.success) { + stopFakeProgress(true) // Complete the progress bar + Modal.success({ + title: t('ovms.download.success'), + content: t('ovms.download.success_desc', { modelName: modelName, modelId: modelId }), + onOk: () => { + setOpen(false) + } + }) + } else { + stopFakeProgress(false) // Reset progress on error + logger.error(`Download failed, is it cancelled? ${cancelled}`) + // Only show error if not cancelled by user + if (!cancelled) { + Modal.error({ + title: t('ovms.download.error'), + content:
, + onOk: () => { + // Keep the form open for retry + } + }) + } + } + } catch (error: any) { + stopFakeProgress(false) // Reset progress on error + logger.error(`Download crashed, is it cancelled? ${cancelled}`) + // Only show error if not cancelled by user + if (!cancelled) { + Modal.error({ + title: t('ovms.download.error'), + content: error.message, + onOk: () => { + // Keep the form open for retry + } + }) + } + } finally { + setLoading(false) + } + } + + return ( + +
+ + ({ + value: model.modelId, + label: model.label + }))} + onSelect={handlePresetSelect} + onChange={handleModelIdChange} + disabled={loading} + allowClear + /> + + + + + + + + {loading && ( + + `${percent}%`} + /> +
+ {t('ovms.download.tip')} +
+
+ )} + + + + + +
+
+ ) +} + +export default class DownloadOVMSModelPopup { + static topviewId = 0 + static hide() { + TopView.hide('DownloadOVMSModelPopup') + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'DownloadOVMSModelPopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx index 2d2af3788a..2e06cc6e73 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx @@ -8,6 +8,7 @@ import { getProviderLabel } from '@renderer/i18n/label' import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings' import EditModelPopup from '@renderer/pages/settings/ProviderSettings/EditModelPopup/EditModelPopup' import AddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/AddModelPopup' +import DownloadOVMSModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup' import ManageModelsPopup from '@renderer/pages/settings/ProviderSettings/ModelList/ManageModelsPopup' import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup' import { Model } from '@renderer/types' @@ -93,6 +94,11 @@ const ModelList: React.FC = ({ providerId }) => { } }, [provider, t]) + const onDownloadModel = useCallback( + () => DownloadOVMSModelPopup.show({ title: t('ovms.download.title'), provider }), + [provider, t] + ) + const isLoading = useMemo(() => displayedModelGroups === null, [displayedModelGroups]) return ( @@ -167,9 +173,15 @@ const ModelList: React.FC = ({ providerId }) => { - + {provider.id !== 'ovms' ? ( + + ) : ( + + )} ) diff --git a/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx new file mode 100644 index 0000000000..827e4097e0 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx @@ -0,0 +1,170 @@ +import { VStack } from '@renderer/components/Layout' +import { Alert, Button } from 'antd' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingRow, SettingSubtitle } from '..' + +const OVMSSettings: FC = () => { + const { t } = useTranslation() + + const [ovmsStatus, setOvmsStatus] = useState<'not-installed' | 'not-running' | 'running'>('not-running') + const [isInstallingOvms, setIsInstallingOvms] = useState(false) + const [isRunningOvms, setIsRunningOvms] = useState(false) + const [isStoppingOvms, setIsStoppingOvms] = useState(false) + + useEffect(() => { + const checkStatus = async () => { + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + } + checkStatus() + }, []) + + const installOvms = async () => { + try { + setIsInstallingOvms(true) + await window.api.installOvmsBinary() + // 安装成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsInstallingOvms(false) + } catch (error: any) { + const errCodeMsg = { + '100': t('ovms.failed.install_code_100'), + '101': t('ovms.failed.install_code_101'), + '102': t('ovms.failed.install_code_102'), + '103': t('ovms.failed.install_code_103'), + '104': t('ovms.failed.install_code_104'), + '105': t('ovms.failed.install_code_105') + } + const match = error.message.match(/code (\d+)/) + const code = match ? match[1] : 'unknown' + const errorMsg = errCodeMsg[code as keyof typeof errCodeMsg] || error.message + + window.toast.error(t('ovms.failed.install') + errorMsg) + setIsInstallingOvms(false) + } + } + + const runOvms = async () => { + try { + setIsRunningOvms(true) + await window.api.ovms.runOvms() + // 运行成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsRunningOvms(false) + } catch (error: any) { + window.toast.error(t('ovms.failed.run') + error.message) + setIsRunningOvms(false) + } + } + + const stopOvms = async () => { + try { + setIsStoppingOvms(true) + await window.api.ovms.stopOvms() + // 停止成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsStoppingOvms(false) + } catch (error: any) { + window.toast.error(t('ovms.failed.stop') + error.message) + setIsStoppingOvms(false) + } + } + + const getAlertType = () => { + switch (ovmsStatus) { + case 'running': + return 'success' + case 'not-running': + return 'warning' + case 'not-installed': + return 'error' + default: + return 'warning' + } + } + + const getStatusMessage = () => { + switch (ovmsStatus) { + case 'running': + return t('ovms.status.running') + case 'not-running': + return t('ovms.status.not_running') + case 'not-installed': + return t('ovms.status.not_installed') + default: + return t('ovms.status.unknown') + } + } + + return ( + <> + + + {getStatusMessage()} + {ovmsStatus === 'not-installed' && ( + + )} + {ovmsStatus === 'not-running' && ( +
+ + +
+ )} + {ovmsStatus === 'running' && ( + + )} +
+ + } + /> +
} + showIcon + /> + + ) +} + +export default OVMSSettings diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index d023f3d0d9..5dc53ca75a 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -27,6 +27,8 @@ import UrlSchemaInfoPopup from './UrlSchemaInfoPopup' const logger = loggerService.withContext('ProviderList') const BUTTON_WRAPPER_HEIGHT = 50 +const systemType = await window.api.system.getDeviceType() +const cpuName = await window.api.system.getCpuName() const ProviderList: FC = () => { const [searchParams, setSearchParams] = useSearchParams() @@ -273,6 +275,10 @@ const ProviderList: FC = () => { } const filteredProviders = providers.filter((provider) => { + if (provider.id === 'ovms' && (systemType !== 'windows' || !cpuName.toLowerCase().includes('intel'))) { + return false + } + const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) const isProviderMatch = matchKeywordsInProvider(keywords, provider) const isModelMatch = provider.models.some((model) => matchKeywordsInModel(keywords, model)) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index ea40f6d9ac..38d6f439e7 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -47,6 +47,7 @@ import DMXAPISettings from './DMXAPISettings' import GithubCopilotSettings from './GithubCopilotSettings' import GPUStackSettings from './GPUStackSettings' import LMStudioSettings from './LMStudioSettings' +import OVMSSettings from './OVMSSettings' import ProviderOAuth from './ProviderOAuth' import SelectProviderModelPopup from './SelectProviderModelPopup' import VertexAISettings from './VertexAISettings' @@ -282,6 +283,7 @@ const ProviderSetting: FC = ({ providerId }) => { {isProviderSupportAuth(provider) && } {provider.id === 'openai' && } + {provider.id === 'ovms' && } {isDmxapi && } {provider.id === 'anthropic' && ( <> diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 1c954ba27e..f1e76ed956 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2549,6 +2549,15 @@ const migrateConfig = { logger.error('migrate 158 error', error as Error) return state } + }, + '159': (state: RootState) => { + try { + addProvider(state, 'ovms') + return state + } catch (error) { + logger.error('migrate 159 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 2b9271d548..c3b7754e65 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -272,6 +272,7 @@ export const SystemProviderIds = { // cherryin: 'cherryin', silicon: 'silicon', aihubmix: 'aihubmix', + ovms: 'ovms', ocoolai: 'ocoolai', deepseek: 'deepseek', ppio: 'ppio', From 23f61b0d6211359571a46c0ace061e8d6c109939 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:22:25 +0800 Subject: [PATCH 2/2] feat: support gpt-5-codex (#10448) * feat(models): add gpt5_codex model support Add support for gpt5_codex model type in model configuration and type definitions. Update getThinkModelType to handle codex variant of gpt5 models. * feat(models): add gpt-5-codex model logo and update logo mapping Add new GPT-5-Codex model logo image and include it in the logo mapping configuration --- .../src/assets/images/models/gpt-5-codex.png | Bin 0 -> 25983 bytes src/renderer/src/config/models/logo.ts | 5 ++++- src/renderer/src/config/models/reasoning.ts | 9 ++++++++- src/renderer/src/types/index.ts | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/assets/images/models/gpt-5-codex.png diff --git a/src/renderer/src/assets/images/models/gpt-5-codex.png b/src/renderer/src/assets/images/models/gpt-5-codex.png new file mode 100644 index 0000000000000000000000000000000000000000..688d187349d1db41b72008b3da01ec01766253b3 GIT binary patch literal 25983 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 { let thinkingModelType: ThinkingModelType = 'default' + const modelId = getLowerBaseModelName(model.id) if (isGPT5SeriesModel(model)) { - thinkingModelType = 'gpt5' + if (modelId.includes('codex')) { + thinkingModelType = 'gpt5_codex' + } else { + thinkingModelType = 'gpt5' + } } else if (isSupportedReasoningEffortOpenAIModel(model)) { thinkingModelType = 'o' } else if (isSupportedThinkingTokenGeminiModel(model)) { diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c3b7754e65..3f38b57749 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -76,6 +76,7 @@ const ThinkModelTypes = [ 'default', 'o', 'gpt5', + 'gpt5_codex', 'grok', 'gemini', 'gemini_pro',