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
This commit is contained in:
Louis 2025-02-24 11:29:11 +07:00 committed by GitHub
parent 18e289e8f7
commit bc2f382e64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 116 additions and 401 deletions

View File

@ -228,7 +228,7 @@ export abstract class BaseExtension implements ExtensionType {
const settings = await this.getSettings() const settings = await this.getSettings()
const updatedSettings = settings.map((setting) => { let updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find( const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key (componentProp) => componentProp.key === setting.key
) )
@ -238,13 +238,20 @@ export abstract class BaseExtension implements ExtensionType {
return setting return setting
}) })
const settingPath = await joinPath([ if (!updatedSettings.length) updatedSettings = componentProps as SettingComponentProps[]
const settingFolder = await joinPath([
await getJanDataFolderPath(), await getJanDataFolderPath(),
this.settingFolderName, this.settingFolderName,
this.name, 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)) await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))
updatedSettings.forEach((setting) => { updatedSettings.forEach((setting) => {

View File

@ -8,7 +8,7 @@
"author": "Jan <service@jan.ai>", "author": "Jan <service@jan.ai>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "jest", "test": "vitest run",
"build": "rolldown -c rolldown.config.mjs", "build": "rolldown -c rolldown.config.mjs",
"codesign:darwin": "../../.github/scripts/auto-sign.sh", "codesign:darwin": "../../.github/scripts/auto-sign.sh",
"codesign:win32:linux": "echo 'No codesigning required'", "codesign:win32:linux": "echo 'No codesigning required'",
@ -25,7 +25,8 @@
"rolldown": "^1.0.0-beta.1", "rolldown": "^1.0.0-beta.1",
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"vitest": "^3.0.6"
}, },
"dependencies": { "dependencies": {
"@janhq/core": "../../core/package.tgz", "@janhq/core": "../../core/package.tgz",

View File

@ -15,7 +15,7 @@
}, },
"transform_resp": { "transform_resp": {
"chat_completions": { "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" "explore_models_url": "https://docs.anthropic.com/en/docs/about-claude/models"

View File

@ -25,6 +25,7 @@ export default defineConfig([
DEFAULT_REQUEST_HEADERS_TRANSFORM: JSON.stringify( DEFAULT_REQUEST_HEADERS_TRANSFORM: JSON.stringify(
'Authorization: Bearer {{api_key}}' 'Authorization: Bearer {{api_key}}'
), ),
VERSION: JSON.stringify(pkgJson.version ?? '0.0.0'),
}, },
}, },
{ {

View File

@ -5,6 +5,7 @@ declare const NODE: string
declare const DEFAULT_REQUEST_PAYLOAD_TRANSFORM: string declare const DEFAULT_REQUEST_PAYLOAD_TRANSFORM: string
declare const DEFAULT_RESPONSE_BODY_TRANSFORM: string declare const DEFAULT_RESPONSE_BODY_TRANSFORM: string
declare const DEFAULT_REQUEST_HEADERS_TRANSFORM: string declare const DEFAULT_REQUEST_HEADERS_TRANSFORM: string
declare const VERSION: string
declare const DEFAULT_REMOTE_ENGINES: ({ declare const DEFAULT_REMOTE_ENGINES: ({
id: string id: string

View File

@ -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()
})
})

View File

@ -44,6 +44,9 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
// Populate default remote engines // Populate default remote engines
this.populateDefaultRemoteEngines() this.populateDefaultRemoteEngines()
// Migrate
this.migrate()
} }
/** /**
@ -373,4 +376,36 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
}) })
.catch(console.info) .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<string>('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,
},
},
])
}
}
} }

View File

@ -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`
)
})
})
})

View File

@ -1,7 +1,7 @@
{ {
"name": "@janhq/model-extension", "name": "@janhq/model-extension",
"productName": "Model Management", "productName": "Model Management",
"version": "1.0.35", "version": "1.0.36",
"description": "Handles model lists, their details, and settings.", "description": "Handles model lists, their details, and settings.",
"main": "dist/index.js", "main": "dist/index.js",
"author": "Jan <service@jan.ai>", "author": "Jan <service@jan.ai>",

View File

@ -438,7 +438,9 @@ export default class JanModelExtension extends ModelExtension {
methods: ['get'], methods: ['get'],
}, },
}) })
.then(() => {}) .then(() => {
this.queue.concurrency = Infinity
})
} }
/** /**

View File

@ -1,17 +1,3 @@
NEXT_PUBLIC_ENV=development NEXT_PUBLIC_ENV=development
NEXT_PUBLIC_WEB_URL= NEXT_PUBLIC_WEB_URL=
NEXT_PUBLIC_DISCORD_INVITATION_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=