From bc2f382e642f6882f70d9eab67754f048b553970 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 24 Feb 2025 11:29:11 +0700 Subject: [PATCH] chore: migrate engine settings on update (#4719) * chore: migrate engine settings on update * chore: queue engine migration to ensure it only execute when server is on * chore: ensure queue is empty instead of running in the queue --- core/src/browser/extension.ts | 13 +- .../engine-management-extension/package.json | 5 +- .../resources/anthropic.json | 2 +- .../rolldown.config.mjs | 1 + .../src/@types/global.d.ts | 1 + .../src/index.test.ts | 60 +++ .../engine-management-extension/src/index.ts | 35 ++ .../src/node/index.test.ts | 378 ------------------ extensions/model-extension/package.json | 2 +- extensions/model-extension/src/index.ts | 4 +- web/env-example | 16 +- 11 files changed, 116 insertions(+), 401 deletions(-) create mode 100644 extensions/engine-management-extension/src/index.test.ts delete mode 100644 extensions/engine-management-extension/src/node/index.test.ts diff --git a/core/src/browser/extension.ts b/core/src/browser/extension.ts index d768473c9..f68f37fa0 100644 --- a/core/src/browser/extension.ts +++ b/core/src/browser/extension.ts @@ -228,7 +228,7 @@ export abstract class BaseExtension implements ExtensionType { const settings = await this.getSettings() - const updatedSettings = settings.map((setting) => { + let updatedSettings = settings.map((setting) => { const updatedSetting = componentProps.find( (componentProp) => componentProp.key === setting.key ) @@ -238,13 +238,20 @@ export abstract class BaseExtension implements ExtensionType { return setting }) - const settingPath = await joinPath([ + if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[] + + const settingFolder = await joinPath([ await getJanDataFolderPath(), this.settingFolderName, this.name, - this.settingFileName, ]) + if (!(await fs.existsSync(settingFolder))) { + await fs.mkdir(settingFolder) + } + + const settingPath = await joinPath([settingFolder, this.settingFileName]) + await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2)) updatedSettings.forEach((setting) => { diff --git a/extensions/engine-management-extension/package.json b/extensions/engine-management-extension/package.json index 571a3852b..f5cc35771 100644 --- a/extensions/engine-management-extension/package.json +++ b/extensions/engine-management-extension/package.json @@ -8,7 +8,7 @@ "author": "Jan ", "license": "MIT", "scripts": { - "test": "jest", + "test": "vitest run", "build": "rolldown -c rolldown.config.mjs", "codesign:darwin": "../../.github/scripts/auto-sign.sh", "codesign:win32:linux": "echo 'No codesigning required'", @@ -25,7 +25,8 @@ "rolldown": "^1.0.0-beta.1", "run-script-os": "^1.1.6", "ts-loader": "^9.5.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.0.6" }, "dependencies": { "@janhq/core": "../../core/package.tgz", diff --git a/extensions/engine-management-extension/resources/anthropic.json b/extensions/engine-management-extension/resources/anthropic.json index 4172bcd0b..771a2c9ff 100644 --- a/extensions/engine-management-extension/resources/anthropic.json +++ b/extensions/engine-management-extension/resources/anthropic.json @@ -15,7 +15,7 @@ }, "transform_resp": { "chat_completions": { - "template": "{% if input_request.stream %} {\"object\": \"chat.completion.chunk\", \"model\": \"{{ input_request.model }}\", \"choices\": [{\"index\": 0, \"delta\": { {% if input_request.type == \"message_start\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"ping\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"content_block_delta\" %} \"role\": \"assistant\", \"content\": \"{{ input_request.delta.text }}\" {% else if input_request.type == \"content_block_stop\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"content_block_stop\" %} \"role\": \"assistant\", \"content\": null {% endif %} }, {% if input_request.type == \"content_block_stop\" %} \"finish_reason\": \"stop\" {% else %} \"finish_reason\": null {% endif %} }]} {% else %} {{tojson(input_request)}} {% endif %}" + "template": "{% if input_request.stream %} {\"object\": \"chat.completion.chunk\", \"model\": \"{{ input_request.model }}\", \"choices\": [{\"index\": 0, \"delta\": { {% if input_request.type == \"message_start\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"ping\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"content_block_delta\" %} \"role\": \"assistant\", \"content\": \"{{ tojson(input_request.delta.text) }}\" {% else if input_request.type == \"content_block_stop\" %} \"role\": \"assistant\", \"content\": null {% else if input_request.type == \"content_block_stop\" %} \"role\": \"assistant\", \"content\": null {% endif %} }, {% if input_request.type == \"content_block_stop\" %} \"finish_reason\": \"stop\" {% else %} \"finish_reason\": null {% endif %} }]} {% else %} {{tojson(input_request)}} {% endif %}" } }, "explore_models_url": "https://docs.anthropic.com/en/docs/about-claude/models" diff --git a/extensions/engine-management-extension/rolldown.config.mjs b/extensions/engine-management-extension/rolldown.config.mjs index 0918c5ad7..7e8322698 100644 --- a/extensions/engine-management-extension/rolldown.config.mjs +++ b/extensions/engine-management-extension/rolldown.config.mjs @@ -25,6 +25,7 @@ export default defineConfig([ DEFAULT_REQUEST_HEADERS_TRANSFORM: JSON.stringify( 'Authorization: Bearer {{api_key}}' ), + VERSION: JSON.stringify(pkgJson.version ?? '0.0.0'), }, }, { diff --git a/extensions/engine-management-extension/src/@types/global.d.ts b/extensions/engine-management-extension/src/@types/global.d.ts index 69ed0cfb6..0dbed3806 100644 --- a/extensions/engine-management-extension/src/@types/global.d.ts +++ b/extensions/engine-management-extension/src/@types/global.d.ts @@ -5,6 +5,7 @@ declare const NODE: string declare const DEFAULT_REQUEST_PAYLOAD_TRANSFORM: string declare const DEFAULT_RESPONSE_BODY_TRANSFORM: string declare const DEFAULT_REQUEST_HEADERS_TRANSFORM: string +declare const VERSION: string declare const DEFAULT_REMOTE_ENGINES: ({ id: string diff --git a/extensions/engine-management-extension/src/index.test.ts b/extensions/engine-management-extension/src/index.test.ts new file mode 100644 index 000000000..78d71ab34 --- /dev/null +++ b/extensions/engine-management-extension/src/index.test.ts @@ -0,0 +1,60 @@ +import { describe, beforeEach, it, expect, vi } from 'vitest' +import JanEngineManagementExtension from './index' +import { Engines, InferenceEngine } from '@janhq/core' + +vi.stubGlobal('API_URL', 'http://localhost:3000') + +const mockEngines: Engines = [ + { + name: 'variant1', + version: '1.0.0', + type: 'local', + engine: InferenceEngine.cortex_llamacpp, + }, +] + +vi.stubGlobal('DEFAULT_REMOTE_ENGINES', mockEngines) + +describe('migrate engine settings', () => { + let extension: JanEngineManagementExtension + + beforeEach(() => { + // @ts-ignore + extension = new JanEngineManagementExtension() + }) + + it('engines should be migrated', async () => { + vi.stubGlobal('VERSION', '2.0.0') + + vi.spyOn(extension, 'getEngines').mockResolvedValue([]) + const mockUpdateEngines = vi + .spyOn(extension, 'updateEngine') + .mockReturnThis() + + mockUpdateEngines.mockResolvedValue({ + messages: 'OK', + }) + + await extension.migrate() + + // Assert that the returned value is equal to the mockEngines object + expect(mockUpdateEngines).toBeCalled() + }) + + it('should not migrate when extesion version is not updated', async () => { + vi.stubGlobal('VERSION', '0.0.0') + vi.spyOn(extension, 'getEngines').mockResolvedValue([]) + const mockUpdateEngines = vi + .spyOn(extension, 'updateEngine') + .mockReturnThis() + + mockUpdateEngines.mockResolvedValue({ + messages: 'OK', + }) + + await extension.migrate() + + // Assert that the returned value is equal to the mockEngines object + expect(mockUpdateEngines).not.toBeCalled() + }) +}) diff --git a/extensions/engine-management-extension/src/index.ts b/extensions/engine-management-extension/src/index.ts index e2730cc71..518e1c36f 100644 --- a/extensions/engine-management-extension/src/index.ts +++ b/extensions/engine-management-extension/src/index.ts @@ -44,6 +44,9 @@ export default class JanEngineManagementExtension extends EngineManagementExtens // Populate default remote engines this.populateDefaultRemoteEngines() + + // Migrate + this.migrate() } /** @@ -373,4 +376,36 @@ export default class JanEngineManagementExtension extends EngineManagementExtens }) .catch(console.info) } + + /** + * Update engine settings to the latest version + */ + migrate = async () => { + // Ensure health check is done + await this.queue.onEmpty() + + const version = await this.getSetting('version', '0.0.0') + const engines = await this.getEngines() + if (version < VERSION) { + + console.log('Migrating engine settings...') + // Migrate engine settings + await Promise.all( + DEFAULT_REMOTE_ENGINES.map((engine) => { + const { id, ...data } = engine + + data.api_key = engines[id]?.api_key + return this.updateEngine(data).catch(console.error) + }) + ) + await this.updateSettings([ + { + key: 'version', + controllerProps: { + value: VERSION, + }, + }, + ]) + } + } } diff --git a/extensions/engine-management-extension/src/node/index.test.ts b/extensions/engine-management-extension/src/node/index.test.ts deleted file mode 100644 index aa2ac8be8..000000000 --- a/extensions/engine-management-extension/src/node/index.test.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { describe, expect, it } from '@jest/globals' -import engine from './index' -import { GpuSetting } from '@janhq/core' -import { fork } from 'child_process' - -let testSettings: GpuSetting = { - run_mode: 'cpu', - vulkan: false, - cuda: { - exist: false, - version: '11', - }, - gpu_highest_vram: '0', - gpus: [], - gpus_in_use: [], - is_initial: false, - notify: true, - nvidia_driver: { - exist: false, - version: '11', - }, -} -const originalPlatform = process.platform - - - -jest.mock('@janhq/core', () => ({ - appResourcePath: () => '.', - log: jest.fn(), -})) - -describe('test executable cortex file', () => { - afterAll(function () { - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }) - }) - - it('executes on MacOS', () => { - - Object.defineProperty(process, 'platform', { - value: 'darwin', - }) - Object.defineProperty(process, 'arch', { - value: 'arm64', - }) - - - expect(engine.engineVariant(testSettings)).resolves.toEqual('mac-arm64') - }) - - it('executes on MacOS', () => { - Object.defineProperty(process, 'platform', { - value: 'darwin', - }) - Object.defineProperty(process, 'arch', { - value: 'arm64', - }) - - const mockProcess = { - on: jest.fn((event, callback) => { - if (event === 'message') { - callback('noavx') - } - }), - send: jest.fn(), - } - - Object.defineProperty(process, 'arch', { - value: 'x64', - }) - - expect(engine.engineVariant(testSettings)).resolves.toEqual('mac-amd64') - }) - - it('executes on Windows CPU', () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'cpu', - } - const mockProcess = { - on: jest.fn((event, callback) => { - if (event === 'message') { - callback('avx') - } - }), - send: jest.fn(), - } - - expect(engine.engineVariant()).resolves.toEqual('windows-amd64-avx') - }) - - it('executes on Windows Cuda 11', () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '11', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - - const mockProcess = { - on: jest.fn((event, callback) => { - if (event === 'message') { - callback('avx2') - } - }), - send: jest.fn(), - } - - expect(engine.engineVariant(settings)).resolves.toEqual( - 'windows-amd64-avx2-cuda-11-7' - ) - }) - - it('executes on Windows Cuda 12', () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '12', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - - expect(engine.engineVariant(settings)).resolves.toEqual( - 'windows-amd64-noavx-cuda-12-0' - ) - - expect(engine.engineVariant(settings)).resolves.toEqual( - 'windows-amd64-avx2-cuda-12-0' - ) - }) - - it('executes on Linux CPU', () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'cpu', - } - - expect(engine.engineVariant()).resolves.toEqual('linux-amd64-noavx') - }) - - it('executes on Linux Cuda 11', () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '11', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - expect(engine.engineVariant(settings)).resolves.toBe( - 'linux-amd64-avx2-cuda-11-7' - ) - }) - - it('executes on Linux Cuda 12', () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '12', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - - - expect(engine.engineVariant(settings)).resolves.toEqual( - 'linux-amd64-avx2-cuda-12-0' - ) - }) - - // Generate test for different cpu instructions on Linux - it(`executes on Linux CPU with different instructions`, () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'cpu', - } - - const cpuInstructions = ['avx512', 'avx2', 'avx', 'noavx'] - cpuInstructions.forEach((instruction) => { - expect(engine.engineVariant(settings)).resolves.toEqual( - `linux-amd64-${instruction}` - ) - }) - }) - // Generate test for different cpu instructions on Windows - it(`executes on Windows CPU with different instructions`, () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'cpu', - } - const cpuInstructions = ['avx512', 'avx2', 'avx', 'noavx'] - cpuInstructions.forEach((instruction) => { - - expect(engine.engineVariant(settings)).resolves.toEqual( - `windows-amd64-${instruction}` - ) - }) - }) - - // Generate test for different cpu instructions on Windows - it(`executes on Windows GPU with different instructions`, () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '12', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - const cpuInstructions = ['avx512', 'avx2', 'avx', 'noavx'] - cpuInstructions.forEach((instruction) => { - - expect(engine.engineVariant(settings)).resolves.toEqual( - `windows-amd64-${instruction === 'avx512' || instruction === 'avx2' ? 'avx2' : 'noavx'}-cuda-12-0` - ) - }) - }) - - // Generate test for different cpu instructions on Linux - it(`executes on Linux GPU with different instructions`, () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const cpuInstructions = ['avx512', 'avx2', 'avx', 'noavx'] - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - cuda: { - exist: true, - version: '12', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - cpuInstructions.forEach((instruction) => { - - expect(engine.engineVariant(settings)).resolves.toEqual( - `linux-amd64-${instruction === 'avx512' || instruction === 'avx2' ? 'avx2' : 'noavx'}-cuda-12-0` - ) - }) - }) - - // Generate test for different cpu instructions on Linux - it(`executes on Linux Vulkan should not have CPU instructions included`, () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - const cpuInstructions = ['avx512', 'avx2', 'avx', 'noavx'] - const settings: GpuSetting = { - ...testSettings, - run_mode: 'gpu', - vulkan: true, - cuda: { - exist: true, - version: '12', - }, - nvidia_driver: { - exist: true, - version: '12', - }, - gpus_in_use: ['0'], - gpus: [ - { - id: '0', - name: 'NVIDIA GeForce GTX 1080', - vram: '80000000', - }, - ], - } - cpuInstructions.forEach((instruction) => { - - expect(engine.engineVariant(settings)).resolves.toEqual( - `linux-amd64-vulkan` - ) - }) - }) -}) diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 0ed76d7e4..32ef2f70c 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/model-extension", "productName": "Model Management", - "version": "1.0.35", + "version": "1.0.36", "description": "Handles model lists, their details, and settings.", "main": "dist/index.js", "author": "Jan ", diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index e8d03ab48..741b72d6b 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -438,7 +438,9 @@ export default class JanModelExtension extends ModelExtension { methods: ['get'], }, }) - .then(() => {}) + .then(() => { + this.queue.concurrency = Infinity + }) } /** diff --git a/web/env-example b/web/env-example index 8bd196ed7..11e735cf6 100644 --- a/web/env-example +++ b/web/env-example @@ -1,17 +1,3 @@ NEXT_PUBLIC_ENV=development NEXT_PUBLIC_WEB_URL= -NEXT_PUBLIC_DISCORD_INVITATION_URL= -NEXT_PUBLIC_DOWNLOAD_APP_IOS= -NEXT_PUBLIC_DOWNLOAD_APP_ANDROID= -NEXT_PUBLIC_GRAPHQL_ENGINE_URL= -NEXT_PUBLIC_GRAPHQL_ENGINE_WEB_SOCKET_URL= -KEYCLOAK_CLIENT_ID= -KEYCLOAK_CLIENT_SECRET= -AUTH_ISSUER= -NEXTAUTH_URL= -NEXTAUTH_SECRET= -END_SESSION_URL= -REFRESH_TOKEN_URL= -// For codegen only -HASURA_ADMIN_TOKEN= -NEXT_PUBLIC_OPENAPI_ENDPOINT= \ No newline at end of file +NEXT_PUBLIC_DISCORD_INVITATION_URL= \ No newline at end of file