From b7a86a0f1d0731d6f72cc986f9e26c7d4462702f Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 30 Dec 2024 22:05:01 +0700 Subject: [PATCH 01/31] feat: remote engine management --- .../browser/extensions/enginesManagement.ts | 20 +- core/src/types/engine/index.ts | 25 ++ .../engine-management-extension/src/index.ts | 31 +- web/helpers/atoms/Extension.atom.ts | 8 + web/hooks/useEngineManagement.ts | 42 ++- web/package.json | 5 +- .../Settings/Engines/LocalEngineItem.tsx | 88 +++++ .../{Settings.tsx => LocalEngineSettings.tsx} | 5 +- .../Settings/Engines/ModalAddRemoteEngine.tsx | 228 +++++++++++++ .../Settings/Engines/RemoteEngineItem.tsx | 84 +++++ .../Settings/Engines/RemoteEngineSettings.tsx | 305 ++++++++++++++++++ web/screens/Settings/Engines/index.tsx | 266 ++------------- web/screens/Settings/SettingDetail/index.tsx | 22 +- .../Settings/SettingLeftPanel/index.tsx | 82 +++-- web/utils/modelEngine.test.ts | 8 +- web/utils/modelEngine.ts | 6 +- yarn.lock | 23 +- 17 files changed, 943 insertions(+), 305 deletions(-) create mode 100644 web/screens/Settings/Engines/LocalEngineItem.tsx rename web/screens/Settings/Engines/{Settings.tsx => LocalEngineSettings.tsx} (99%) create mode 100644 web/screens/Settings/Engines/ModalAddRemoteEngine.tsx create mode 100644 web/screens/Settings/Engines/RemoteEngineItem.tsx create mode 100644 web/screens/Settings/Engines/RemoteEngineSettings.tsx diff --git a/core/src/browser/extensions/enginesManagement.ts b/core/src/browser/extensions/enginesManagement.ts index 524546b053..88120b563f 100644 --- a/core/src/browser/extensions/enginesManagement.ts +++ b/core/src/browser/extensions/enginesManagement.ts @@ -3,6 +3,7 @@ import { Engines, EngineVariant, EngineReleased, + EngineConfig, DefaultEngineVariant, } from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' @@ -54,10 +55,7 @@ export abstract class EngineManagementExtension extends BaseExtension { * @param name - Inference engine name. * @returns A Promise that resolves to intall of engine. */ - abstract installEngine( - name: InferenceEngine, - engineConfig: { variant: string; version?: string } - ): Promise<{ messages: string }> + abstract installEngine(name: string, engineConfig: EngineConfig): Promise<{ messages: string }> /** * @param name - Inference engine name. @@ -65,7 +63,7 @@ export abstract class EngineManagementExtension extends BaseExtension { */ abstract uninstallEngine( name: InferenceEngine, - engineConfig: { variant: string; version: string } + engineConfig: EngineConfig ): Promise<{ messages: string }> /** @@ -81,11 +79,19 @@ export abstract class EngineManagementExtension extends BaseExtension { */ abstract setDefaultEngineVariant( name: InferenceEngine, - engineConfig: { variant: string; version: string } + engineConfig: EngineConfig ): Promise<{ messages: string }> /** * @returns A Promise that resolves to update engine. */ - abstract updateEngine(name: InferenceEngine): Promise<{ messages: string }> + abstract updateEngine( + name: InferenceEngine, + engineConfig?: EngineConfig + ): Promise<{ messages: string }> + + /** + * @returns A Promise that resolves to an object of remote models list . + */ + abstract getRemoteModels(name: InferenceEngine | string): Promise } diff --git a/core/src/types/engine/index.ts b/core/src/types/engine/index.ts index 9e6f5c9c83..83be19d662 100644 --- a/core/src/types/engine/index.ts +++ b/core/src/types/engine/index.ts @@ -4,6 +4,22 @@ export type Engines = { [key in InferenceEngine]: EngineVariant[] } +export type EngineMetadata = { + get_models_url?: string + api_key_template?: string + transform_req?: { + chat_completions?: { + url?: string + template?: string + } + } + transform_resp?: { + chat_completions?: { + template?: string + } + } +} + export type EngineVariant = { engine: InferenceEngine name: string @@ -23,6 +39,15 @@ export type EngineReleased = { size: number } +export type EngineConfig = { + version?: string + variant?: string + type?: string + url?: string + api_key?: string + metadata?: EngineMetadata +} + export enum EngineEvent { OnEngineUpdate = 'OnEngineUpdate', } diff --git a/extensions/engine-management-extension/src/index.ts b/extensions/engine-management-extension/src/index.ts index 215ae3bc21..079ca4400e 100644 --- a/extensions/engine-management-extension/src/index.ts +++ b/extensions/engine-management-extension/src/index.ts @@ -3,6 +3,7 @@ import { InferenceEngine, DefaultEngineVariant, Engines, + EngineConfig, EngineVariant, EngineReleased, executeOnMain, @@ -81,6 +82,18 @@ export default class JSONEngineManagementExtension extends EngineManagementExten ) as Promise } + /** + * @returns A Promise that resolves to an object of list engines. + */ + async getRemoteModels(name: string): Promise { + return this.queue.add(() => + ky + .get(`${API_URL}/v1/models/remote/${name}`) + .json() + .then((e) => e) + ) as Promise + } + /** * @param name - Inference engine name. * @returns A Promise that resolves to an array of installed engine. @@ -135,10 +148,7 @@ export default class JSONEngineManagementExtension extends EngineManagementExten * @param name - Inference engine name. * @returns A Promise that resolves to intall of engine. */ - async installEngine( - name: InferenceEngine, - engineConfig: { variant: string; version?: string } - ) { + async installEngine(name: string, engineConfig: EngineConfig) { return this.queue.add(() => ky .post(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig }) @@ -150,10 +160,7 @@ export default class JSONEngineManagementExtension extends EngineManagementExten * @param name - Inference engine name. * @returns A Promise that resolves to unintall of engine. */ - async uninstallEngine( - name: InferenceEngine, - engineConfig: { variant: string; version: string } - ) { + async uninstallEngine(name: InferenceEngine, engineConfig: EngineConfig) { return this.queue.add(() => ky .delete(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig }) @@ -181,7 +188,7 @@ export default class JSONEngineManagementExtension extends EngineManagementExten */ async setDefaultEngineVariant( name: InferenceEngine, - engineConfig: { variant: string; version: string } + engineConfig: EngineConfig ) { return this.queue.add(() => ky @@ -193,9 +200,11 @@ export default class JSONEngineManagementExtension extends EngineManagementExten /** * @returns A Promise that resolves to update engine. */ - async updateEngine(name: InferenceEngine) { + async updateEngine(name: InferenceEngine, engineConfig?: EngineConfig) { return this.queue.add(() => - ky.post(`${API_URL}/v1/engines/${name}/update`).then((e) => e) + ky + .post(`${API_URL}/v1/engines/${name}/update`, { json: engineConfig }) + .then((e) => e) ) as Promise<{ messages: string }> } diff --git a/web/helpers/atoms/Extension.atom.ts b/web/helpers/atoms/Extension.atom.ts index 257d429966..3f1843dc40 100644 --- a/web/helpers/atoms/Extension.atom.ts +++ b/web/helpers/atoms/Extension.atom.ts @@ -55,3 +55,11 @@ export const showSettingActiveLocalEngineAtom = atomWithStorage( undefined, { getOnInit: true } ) + +const SHOW_SETTING_ACTIVE_REMOTE_ENGINE = 'showSettingActiveRemoteEngine' +export const showSettingActiveRemoteEngineAtom = atomWithStorage( + SHOW_SETTING_ACTIVE_REMOTE_ENGINE, + [], + undefined, + { getOnInit: true } +) diff --git a/web/hooks/useEngineManagement.ts b/web/hooks/useEngineManagement.ts index 1272da81ad..be690aceca 100644 --- a/web/hooks/useEngineManagement.ts +++ b/web/hooks/useEngineManagement.ts @@ -5,6 +5,7 @@ import { EngineManagementExtension, InferenceEngine, EngineReleased, + EngineConfig, } from '@janhq/core' import { useAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' @@ -61,6 +62,34 @@ export function useGetEngines() { return { engines, error, mutate } } +/** + * @returns A Promise that resolves to an object of remote models. + */ +export function useGetRemoteModels(name: string) { + const extension = useMemo( + () => + extensionManager.get( + ExtensionTypeEnum.Engine + ) ?? null, + [] + ) + + const { + data: remoteModels, + error, + mutate, + } = useSWR( + extension ? 'remoteModels' : null, + () => fetchExtensionData(extension, (ext) => ext.getRemoteModels(name)), + { + revalidateOnFocus: false, + revalidateOnReconnect: true, + } + ) + + return { remoteModels, error, mutate } +} + /** * @param name - Inference engine name. * @returns A Promise that resolves to an array of installed engine. @@ -262,7 +291,10 @@ export const setDefaultEngineVariant = async ( * @body version - string * @returns A Promise that resolves to set default engine. */ -export const updateEngine = async (name: InferenceEngine) => { +export const updateEngine = async ( + name: InferenceEngine, + engineConfig?: EngineConfig +) => { const extension = getExtension() if (!extension) { @@ -271,7 +303,7 @@ export const updateEngine = async (name: InferenceEngine) => { try { // Call the extension's method - const response = await extension.updateEngine(name) + const response = await extension.updateEngine(name, engineConfig) return response } catch (error) { console.error('Failed to set default engine variant:', error) @@ -284,8 +316,8 @@ export const updateEngine = async (name: InferenceEngine) => { * @returns A Promise that resolves to intall of engine. */ export const installEngine = async ( - name: InferenceEngine, - engineConfig: { variant: string; version?: string } + name: string, + engineConfig: EngineConfig ) => { const extension = getExtension() @@ -309,7 +341,7 @@ export const installEngine = async ( */ export const uninstallEngine = async ( name: InferenceEngine, - engineConfig: { variant: string; version: string } + engineConfig: EngineConfig ) => { const extension = getExtension() diff --git a/web/package.json b/web/package.json index 598c748d92..a279c27b85 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "test": "jest" }, "dependencies": { + "@hookform/resolvers": "^3.9.1", "@janhq/core": "link:../core", "@janhq/joi": "link:../joi", "@radix-ui/react-icons": "^1.3.2", @@ -39,6 +40,7 @@ "react-circular-progressbar": "^2.1.0", "react-dom": "18.2.0", "react-dropzone": "14.2.3", + "react-hook-form": "^7.54.2", "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", "react-markdown": "^9.0.1", @@ -57,7 +59,8 @@ "tailwindcss": "3.3.5", "ulidx": "^2.3.0", "use-debounce": "^10.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.24.1" }, "devDependencies": { "@next/eslint-plugin-next": "^14.0.1", diff --git a/web/screens/Settings/Engines/LocalEngineItem.tsx b/web/screens/Settings/Engines/LocalEngineItem.tsx new file mode 100644 index 0000000000..c4b612607e --- /dev/null +++ b/web/screens/Settings/Engines/LocalEngineItem.tsx @@ -0,0 +1,88 @@ +import React, { useCallback } from 'react' + +import { InferenceEngine } from '@janhq/core' +import { Button, Switch, Badge } from '@janhq/joi' + +import { useAtom, useSetAtom } from 'jotai' +import { SettingsIcon } from 'lucide-react' + +import { useGetDefaultEngineVariant } from '@/hooks/useEngineManagement' + +import { getTitleByEngine } from '@/utils/modelEngine' + +import { showSettingActiveLocalEngineAtom } from '@/helpers/atoms/Extension.atom' +import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' + +const LocalEngineItems = ({ engine }: { engine: InferenceEngine }) => { + const { defaultEngineVariant } = useGetDefaultEngineVariant(engine) + + const manualDescription = (engine: string) => { + switch (engine) { + case InferenceEngine.cortex_llamacpp: + return 'Fast, efficient local inference engine that runs GGUFmodels directly on your device' + + default: + break + } + } + + const setSelectedSetting = useSetAtom(selectedSettingAtom) + + const [showSettingActiveLocalEngine, setShowSettingActiveLocalEngineAtom] = + useAtom(showSettingActiveLocalEngineAtom) + + const onSwitchChange = useCallback( + (name: string) => { + if (showSettingActiveLocalEngine.includes(name)) { + setShowSettingActiveLocalEngineAtom( + [...showSettingActiveLocalEngine].filter((x) => x !== name) + ) + } else { + setShowSettingActiveLocalEngineAtom([ + ...showSettingActiveLocalEngine, + name, + ]) + } + }, + [showSettingActiveLocalEngine, setShowSettingActiveLocalEngineAtom] + ) + return ( +
+
+
+
+
+
+ {getTitleByEngine(engine as InferenceEngine)} +
+ + {defaultEngineVariant?.version} + +
+
+

{manualDescription(engine)}

+
+
+
+ onSwitchChange(engine)} + /> + +
+
+
+
+ ) +} + +export default LocalEngineItems diff --git a/web/screens/Settings/Engines/Settings.tsx b/web/screens/Settings/Engines/LocalEngineSettings.tsx similarity index 99% rename from web/screens/Settings/Engines/Settings.tsx rename to web/screens/Settings/Engines/LocalEngineSettings.tsx index 8cc8501efe..2ac2dd5664 100644 --- a/web/screens/Settings/Engines/Settings.tsx +++ b/web/screens/Settings/Engines/LocalEngineSettings.tsx @@ -19,6 +19,7 @@ import { installEngine, updateEngine, useGetReleasedEnginesByVersion, + uninstallEngine, } from '@/hooks/useEngineManagement' import { formatDownloadPercentage } from '@/utils/converter' @@ -36,7 +37,7 @@ const os = () => { } } -const EngineSettings = ({ engine }: { engine: InferenceEngine }) => { +const LocalEngineSettings = ({ engine }: { engine: InferenceEngine }) => { const { installedEngines, mutate: mutateInstalledEngines } = useGetInstalledEngines(engine) const { defaultEngineVariant, mutate: mutateDefaultEngineVariant } = @@ -343,4 +344,4 @@ const EngineSettings = ({ engine }: { engine: InferenceEngine }) => { ) } -export default EngineSettings +export default LocalEngineSettings diff --git a/web/screens/Settings/Engines/ModalAddRemoteEngine.tsx b/web/screens/Settings/Engines/ModalAddRemoteEngine.tsx new file mode 100644 index 0000000000..79fc4dd727 --- /dev/null +++ b/web/screens/Settings/Engines/ModalAddRemoteEngine.tsx @@ -0,0 +1,228 @@ +import { memo, useState } from 'react' +import { useForm } from 'react-hook-form' + +import { zodResolver } from '@hookform/resolvers/zod' + +import { Button, Input, Modal, TextArea } from '@janhq/joi' +import { PlusIcon } from 'lucide-react' +import { z } from 'zod' + +import { installEngine, useGetEngines } from '@/hooks/useEngineManagement' + +const engineSchema = z.object({ + engineName: z.string().min(1, 'Engine name is required'), + apiUrl: z.string().url('Enter a valid API URL'), + modelListUrl: z.string().url('Enter a valid Model List URL'), + apiKeyTemplate: z.string().optional(), + apiKey: z.string().optional(), + requestFormat: z.string().optional(), + responseFormat: z.string().optional(), +}) + +const ModalAddRemoteEngine = () => { + const [open, setOpen] = useState(false) + const { mutate: mutateListEngines } = useGetEngines() + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(engineSchema), + defaultValues: { + engineName: '', + apiUrl: '', + modelListUrl: '', + apiKeyTemplate: '', + apiKey: '', + requestFormat: '', + responseFormat: '', + }, + }) + + const onSubmit = async (data: z.infer) => { + await installEngine(data.engineName, { + type: 'remote', + url: data.apiUrl, + api_key: data.apiKey, + metadata: { + api_key_template: data.apiKeyTemplate, + get_models_url: data.modelListUrl, + transform_req: { + chat_completions: { + template: data.requestFormat, + }, + }, + transform_resp: { + chat_completions: { + template: data.requestFormat, + }, + }, + }, + }) + mutateListEngines() + + setOpen(false) + } + + // Helper to render labels with asterisks for required fields + const renderLabel = (label: string, isRequired: boolean, desc?: string) => ( + <> + + {label} {isRequired && *} + +

+ {desc} +

+ + ) + + return ( + +

Install Remote Engine

+

+ Only OpenAI API-compatible engines are supported +

+ + } + fullPage + open={open} + onOpenChange={() => setOpen(!open)} + trigger={ + + } + content={ +
+
+
+ + + {errors.engineName && ( +

+ {errors.engineName.message} +

+ )} +
+ +
+ + + {errors.apiUrl && ( +

{errors.apiUrl.message}

+ )} +
+ +
+ + + {errors.modelListUrl && ( +

+ {errors.modelListUrl.message} +

+ )} +
+ +
+ + + {errors.apiKeyTemplate && ( +

+ {errors.apiKeyTemplate.message} +

+ )} +
+ +
+ + +
+ +
+ +