From 34a4888613e25374eac35a7038d4d277e5b4b9c5 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 17:40:07 +0700 Subject: [PATCH 1/6] feat: support cortex configs and engines commands --- cortex-js/src/app.module.ts | 6 ++ cortex-js/src/command.module.ts | 24 +++++++ .../src/domain/abstracts/engine.abstract.ts | 2 + .../src/domain/abstracts/oai.abstract.ts | 2 + cortex-js/src/extensions/extensions.module.ts | 25 ++++++++ cortex-js/src/extensions/groq.engine.ts | 30 +++++++++ cortex-js/src/extensions/groq/index.ts | 11 ---- cortex-js/src/extensions/groq/package.json | 31 --------- cortex-js/src/extensions/groq/tsconfig.json | 14 ----- cortex-js/src/extensions/mistral.engine.ts | 30 +++++++++ cortex-js/src/extensions/mistral/index.ts | 11 ---- cortex-js/src/extensions/mistral/package.json | 30 --------- .../src/extensions/mistral/tsconfig.json | 14 ----- cortex-js/src/extensions/openai.engine.ts | 30 +++++++++ cortex-js/src/extensions/openai/index.ts | 11 ---- cortex-js/src/extensions/openai/package.json | 30 --------- cortex-js/src/extensions/openai/tsconfig.json | 14 ----- .../commanders/configs.command.ts | 24 +++++++ .../commanders/configs/configs-get.command.ts | 28 +++++++++ .../configs/configs-list.command.ts | 22 +++++++ .../commanders/configs/configs-set.command.ts | 54 ++++++++++++++++ .../commanders/cortex-command.commander.ts | 4 ++ .../commanders/engines.command.ts | 20 ++++++ .../commanders/engines/engines-get.command.ts | 35 +++++++++++ .../engines/engines-list.command.ts | 22 +++++++ .../commanders/types/engine.interface.ts | 5 ++ .../providers/cortex/cortex.provider.ts | 3 + .../extensions/extension.module.ts | 8 ++- .../extensions/extension.repository.ts | 28 ++++++--- .../file-manager/file-manager.service.ts | 4 +- .../src/usecases/configs/configs.module.ts | 11 ++++ .../src/usecases/configs/configs.usecase.ts | 63 +++++++++++++++++++ .../src/usecases/engines/engines.module.ts | 12 ++++ .../src/usecases/engines/engines.usecase.ts | 31 +++++++++ 34 files changed, 512 insertions(+), 177 deletions(-) create mode 100644 cortex-js/src/extensions/extensions.module.ts create mode 100644 cortex-js/src/extensions/groq.engine.ts delete mode 100644 cortex-js/src/extensions/groq/index.ts delete mode 100644 cortex-js/src/extensions/groq/package.json delete mode 100644 cortex-js/src/extensions/groq/tsconfig.json create mode 100644 cortex-js/src/extensions/mistral.engine.ts delete mode 100644 cortex-js/src/extensions/mistral/index.ts delete mode 100644 cortex-js/src/extensions/mistral/package.json delete mode 100644 cortex-js/src/extensions/mistral/tsconfig.json create mode 100644 cortex-js/src/extensions/openai.engine.ts delete mode 100644 cortex-js/src/extensions/openai/index.ts delete mode 100644 cortex-js/src/extensions/openai/package.json delete mode 100644 cortex-js/src/extensions/openai/tsconfig.json create mode 100644 cortex-js/src/infrastructure/commanders/configs.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/configs/configs-get.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/configs/configs-list.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/configs/configs-set.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/engines.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts create mode 100644 cortex-js/src/infrastructure/commanders/engines/engines-list.command.ts create mode 100644 cortex-js/src/usecases/configs/configs.module.ts create mode 100644 cortex-js/src/usecases/configs/configs.usecase.ts create mode 100644 cortex-js/src/usecases/engines/engines.module.ts create mode 100644 cortex-js/src/usecases/engines/engines.usecase.ts diff --git a/cortex-js/src/app.module.ts b/cortex-js/src/app.module.ts index 6ff7528e3..270c9f110 100644 --- a/cortex-js/src/app.module.ts +++ b/cortex-js/src/app.module.ts @@ -26,6 +26,9 @@ import { StatusController } from './infrastructure/controllers/status.controller import { ProcessController } from './infrastructure/controllers/process.controller'; import { DownloadManagerModule } from './infrastructure/services/download-manager/download-manager.module'; import { ContextModule } from './infrastructure/services/context/context.module'; +import { ExtensionsModule } from './extensions/extensions.module'; +import { ConfigsModule } from './usecases/configs/configs.module'; +import { EnginesModule } from './usecases/engines/engines.module'; @Module({ imports: [ @@ -50,6 +53,9 @@ import { ContextModule } from './infrastructure/services/context/context.module' TelemetryModule, ContextModule, DownloadManagerModule, + ExtensionsModule, + ConfigsModule, + EnginesModule, ], controllers: [ AssistantsController, diff --git a/cortex-js/src/command.module.ts b/cortex-js/src/command.module.ts index f1ff68a95..097590ce4 100644 --- a/cortex-js/src/command.module.ts +++ b/cortex-js/src/command.module.ts @@ -34,6 +34,16 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; import { DownloadManagerModule } from './infrastructure/services/download-manager/download-manager.module'; import { ServeStopCommand } from './infrastructure/commanders/sub-commands/serve-stop.command'; import { ContextModule } from './infrastructure/services/context/context.module'; +import { ExtensionsModule } from './extensions/extensions.module'; +import { ConfigsCommand } from './infrastructure/commanders/configs.command'; +import { EnginesCommand } from './infrastructure/commanders/engines.command'; +import { ConfigsModule } from './usecases/configs/configs.module'; +import { EnginesModule } from './usecases/engines/engines.module'; +import { ConfigsGetCommand } from './infrastructure/commanders/configs/configs-get.command'; +import { ConfigsListCommand } from './infrastructure/commanders/configs/configs-list.command'; +import { ConfigsSetCommand } from './infrastructure/commanders/configs/configs-set.command'; +import { EnginesListCommand } from './infrastructure/commanders/engines/engines-list.command'; +import { EnginesGetCommand } from './infrastructure/commanders/engines/engines-get.command'; @Module({ imports: [ @@ -55,6 +65,9 @@ import { ContextModule } from './infrastructure/services/context/context.module' TelemetryModule, ContextModule, DownloadManagerModule, + ExtensionsModule, + ConfigsModule, + EnginesModule, ], providers: [ CortexCommand, @@ -67,6 +80,7 @@ import { ContextModule } from './infrastructure/services/context/context.module' PresetCommand, EmbeddingCommand, BenchmarkCommand, + EnginesCommand, // Questions InitRunModeQuestions, @@ -88,6 +102,16 @@ import { ContextModule } from './infrastructure/services/context/context.module' // Serve ServeStopCommand, + + // Configs + ConfigsCommand, + ConfigsGetCommand, + ConfigsListCommand, + ConfigsSetCommand, + + // Engines + EnginesListCommand, + EnginesGetCommand, ], }) export class CommandModule {} diff --git a/cortex-js/src/domain/abstracts/engine.abstract.ts b/cortex-js/src/domain/abstracts/engine.abstract.ts index 0b11ee9aa..1e11352bc 100644 --- a/cortex-js/src/domain/abstracts/engine.abstract.ts +++ b/cortex-js/src/domain/abstracts/engine.abstract.ts @@ -6,6 +6,8 @@ import { Extension } from './extension.abstract'; export abstract class EngineExtension extends Extension { abstract provider: string; + abstract onLoad(): void; + abstract inference( dto: any, headers: Record, diff --git a/cortex-js/src/domain/abstracts/oai.abstract.ts b/cortex-js/src/domain/abstracts/oai.abstract.ts index 6f5165d53..ab7bda32c 100644 --- a/cortex-js/src/domain/abstracts/oai.abstract.ts +++ b/cortex-js/src/domain/abstracts/oai.abstract.ts @@ -10,6 +10,8 @@ export abstract class OAIEngineExtension extends EngineExtension { super(); } + override onLoad(): void {} + override async inference( createChatDto: any, headers: Record, diff --git a/cortex-js/src/extensions/extensions.module.ts b/cortex-js/src/extensions/extensions.module.ts new file mode 100644 index 000000000..ccdd41d44 --- /dev/null +++ b/cortex-js/src/extensions/extensions.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import GroqEngineExtension from './groq.engine'; +import MistralEngineExtension from './mistral.engine'; +import OpenAIEngineExtension from './openai.engine'; +import { HttpModule, HttpService } from '@nestjs/axios'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; +import { ConfigsModule } from '@/usecases/configs/configs.module'; + +const provider = { + provide: 'EXTENSIONS_PROVIDER', + inject: [HttpService, ConfigsUsecases], + useFactory: (httpService: HttpService, configUsecases: ConfigsUsecases) => [ + new OpenAIEngineExtension(httpService, configUsecases), + new GroqEngineExtension(httpService, configUsecases), + new MistralEngineExtension(httpService, configUsecases), + ], +}; + +@Module({ + imports: [HttpModule, ConfigsModule], + controllers: [], + providers: [provider], + exports: [provider], +}) +export class ExtensionsModule {} diff --git a/cortex-js/src/extensions/groq.engine.ts b/cortex-js/src/extensions/groq.engine.ts new file mode 100644 index 000000000..276d64a5c --- /dev/null +++ b/cortex-js/src/extensions/groq.engine.ts @@ -0,0 +1,30 @@ +import { HttpService } from '@nestjs/axios'; +import { OAIEngineExtension } from '../domain/abstracts/oai.abstract'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +/** + * A class that implements the InferenceExtension interface from the @janhq/core package. + * The class provides methods for initializing and stopping a model, and for making inference requests. + * It also subscribes to events emitted by the @janhq/core package and handles new message requests. + */ +export default class GroqEngineExtension extends OAIEngineExtension { + provider: string = 'groq'; + apiUrl = 'https://api.groq.com/openai/v1/chat/completions'; + name = 'Groq Inference Engine'; + description = 'This extension enables fast Groq chat completion API calls'; + + constructor( + protected readonly httpService: HttpService, + protected readonly configsUsecases: ConfigsUsecases, + ) { + super(httpService); + } + + async onLoad() { + const configs = (await this.configsUsecases.getGroupConfigs( + this.provider, + )) as unknown as { apiKey: string }; + if (!configs?.apiKey) + await this.configsUsecases.saveConfig('apiKey', '', this.provider); + } +} diff --git a/cortex-js/src/extensions/groq/index.ts b/cortex-js/src/extensions/groq/index.ts deleted file mode 100644 index 6a403bab8..000000000 --- a/cortex-js/src/extensions/groq/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OAIEngineExtension } from './../../domain/abstracts/oai.abstract'; - -/** - * A class that implements the InferenceExtension interface from the @janhq/core package. - * The class provides methods for initializing and stopping a model, and for making inference requests. - * It also subscribes to events emitted by the @janhq/core package and handles new message requests. - */ -export default class GroqEngineExtension extends OAIEngineExtension { - provider: string = 'groq'; - apiUrl = 'https://api.groq.com/openai/v1/chat/completions'; -} diff --git a/cortex-js/src/extensions/groq/package.json b/cortex-js/src/extensions/groq/package.json deleted file mode 100644 index ea8e2579b..000000000 --- a/cortex-js/src/extensions/groq/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@janhq/groq-inference-engine", - "productName": "Groq Inference Engine", - "version": "1.0.0", - "main": "dist/extensions/groq/index.js", - "type": "commonjs", - "description": "This extension enables Groq chat completion API calls", - "author": "Jan ", - "license": "AGPL-3.0", - "scripts": { - "build": "tsc" - }, - "exports": { - ".": "./dist/index.js" - }, - "devDependencies": { - "@types/node": "^20.12.8", - "cpx": "^1.5.0", - "rimraf": "^3.0.2", - "ts-loader": "^9.5.0", - "typescript": "^5.4.5" - }, - "dependencies": {}, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "dist/*" - ], - "bundleDependencies": [] -} diff --git a/cortex-js/src/extensions/groq/tsconfig.json b/cortex-js/src/extensions/groq/tsconfig.json deleted file mode 100644 index 6763872ad..000000000 --- a/cortex-js/src/extensions/groq/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "target": "ES2017", - "strict": true, - "sourceMap": true, - "outDir": "dist", - "moduleResolution": "node", - "declaration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["./**/*"] -} diff --git a/cortex-js/src/extensions/mistral.engine.ts b/cortex-js/src/extensions/mistral.engine.ts new file mode 100644 index 000000000..0c3445f1f --- /dev/null +++ b/cortex-js/src/extensions/mistral.engine.ts @@ -0,0 +1,30 @@ +import { HttpService } from '@nestjs/axios'; +import { OAIEngineExtension } from '../domain/abstracts/oai.abstract'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +/** + * A class that implements the InferenceExtension interface from the @janhq/core package. + * The class provides methods for initializing and stopping a model, and for making inference requests. + * It also subscribes to events emitted by the @janhq/core package and handles new message requests. + */ +export default class MistralEngineExtension extends OAIEngineExtension { + provider: string = 'mistral'; + apiUrl = 'https://api.mistral.ai/v1/chat/completions'; + name = 'Mistral Inference Engine'; + description = 'This extension enables Mistral chat completion API calls'; + + constructor( + protected readonly httpService: HttpService, + protected readonly configsUsecases: ConfigsUsecases, + ) { + super(httpService); + } + + async onLoad() { + const configs = (await this.configsUsecases.getGroupConfigs( + this.provider, + )) as unknown as { apiKey: string }; + if (!configs?.apiKey) + await this.configsUsecases.saveConfig('apiKey', '', this.provider); + } +} diff --git a/cortex-js/src/extensions/mistral/index.ts b/cortex-js/src/extensions/mistral/index.ts deleted file mode 100644 index 3a53b117c..000000000 --- a/cortex-js/src/extensions/mistral/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OAIEngineExtension } from './../../domain/abstracts/oai.abstract'; - -/** - * A class that implements the InferenceExtension interface from the @janhq/core package. - * The class provides methods for initializing and stopping a model, and for making inference requests. - * It also subscribes to events emitted by the @janhq/core package and handles new message requests. - */ -export default class MistralEngineExtension extends OAIEngineExtension { - provider: string = 'mistral'; - apiUrl = 'https://api.mistral.ai/v1/chat/completions'; -} diff --git a/cortex-js/src/extensions/mistral/package.json b/cortex-js/src/extensions/mistral/package.json deleted file mode 100644 index 38255c81a..000000000 --- a/cortex-js/src/extensions/mistral/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@janhq/mistral-inference-engine", - "productName": "Mistral Inference Engine", - "version": "1.0.0", - "main": "dist/extensions/mistral/index.js", - "type": "commonjs", - "description": "This extension enables Mistral chat completion API calls", - "author": "Jan ", - "license": "AGPL-3.0", - "scripts": { - "build": "tsc" - }, - "exports": { - ".": "./dist/index.js" - }, - "devDependencies": { - "cpx": "^1.5.0", - "rimraf": "^3.0.2", - "ts-loader": "^9.5.0", - "typescript": "^5.4.5" - }, - "dependencies": {}, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "dist/*" - ], - "bundleDependencies": [] -} diff --git a/cortex-js/src/extensions/mistral/tsconfig.json b/cortex-js/src/extensions/mistral/tsconfig.json deleted file mode 100644 index 6763872ad..000000000 --- a/cortex-js/src/extensions/mistral/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "target": "ES2017", - "strict": true, - "sourceMap": true, - "outDir": "dist", - "moduleResolution": "node", - "declaration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["./**/*"] -} diff --git a/cortex-js/src/extensions/openai.engine.ts b/cortex-js/src/extensions/openai.engine.ts new file mode 100644 index 000000000..d38ada6cf --- /dev/null +++ b/cortex-js/src/extensions/openai.engine.ts @@ -0,0 +1,30 @@ +import { HttpService } from '@nestjs/axios'; +import { OAIEngineExtension } from '../domain/abstracts/oai.abstract'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +/** + * A class that implements the InferenceExtension interface from the @janhq/core package. + * The class provides methods for initializing and stopping a model, and for making inference requests. + * It also subscribes to events emitted by the @janhq/core package and handles new message requests. + */ +export default class OpenAIEngineExtension extends OAIEngineExtension { + provider: string = 'openai'; + apiUrl = 'https://api.openai.com/v1/chat/completions'; + name = 'OpenAI Inference Engine'; + description? = 'This extension enables OpenAI chat completion API calls'; + + constructor( + protected readonly httpService: HttpService, + protected readonly configsUsecases: ConfigsUsecases, + ) { + super(httpService); + } + + async onLoad() { + const configs = (await this.configsUsecases.getGroupConfigs( + this.provider, + )) as unknown as { apiKey: string }; + if (!configs?.apiKey) + await this.configsUsecases.saveConfig('apiKey', '', this.provider); + } +} diff --git a/cortex-js/src/extensions/openai/index.ts b/cortex-js/src/extensions/openai/index.ts deleted file mode 100644 index c2ba67897..000000000 --- a/cortex-js/src/extensions/openai/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OAIEngineExtension } from './../../domain/abstracts/oai.abstract'; - -/** - * A class that implements the InferenceExtension interface from the @janhq/core package. - * The class provides methods for initializing and stopping a model, and for making inference requests. - * It also subscribes to events emitted by the @janhq/core package and handles new message requests. - */ -export default class OpenAIEngineExtension extends OAIEngineExtension { - provider: string = 'openai'; - apiUrl = 'https://api.openai.com/v1/chat/completions'; -} diff --git a/cortex-js/src/extensions/openai/package.json b/cortex-js/src/extensions/openai/package.json deleted file mode 100644 index 3fa4f179d..000000000 --- a/cortex-js/src/extensions/openai/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@janhq/openai-inference-engine", - "productName": "OpenAI Inference Engine", - "version": "1.0.0", - "main": "dist/extensions/openai/index.js", - "type": "commonjs", - "description": "This extension enables OpenAI chat completion API calls", - "author": "Jan ", - "license": "AGPL-3.0", - "scripts": { - "build": "tsc" - }, - "exports": { - ".": "./dist/index.js" - }, - "devDependencies": { - "cpx": "^1.5.0", - "rimraf": "^3.0.2", - "ts-loader": "^9.5.0", - "typescript": "^5.4.5" - }, - "dependencies": {}, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "dist/*" - ], - "bundleDependencies": [] -} diff --git a/cortex-js/src/extensions/openai/tsconfig.json b/cortex-js/src/extensions/openai/tsconfig.json deleted file mode 100644 index 6763872ad..000000000 --- a/cortex-js/src/extensions/openai/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "target": "ES2017", - "strict": true, - "sourceMap": true, - "outDir": "dist", - "moduleResolution": "node", - "declaration": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["./**/*"] -} diff --git a/cortex-js/src/infrastructure/commanders/configs.command.ts b/cortex-js/src/infrastructure/commanders/configs.command.ts new file mode 100644 index 000000000..01f797b1a --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/configs.command.ts @@ -0,0 +1,24 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from './decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { ConfigsGetCommand } from './configs/configs-get.command'; +import { ConfigsListCommand } from './configs/configs-list.command'; +import { ConfigsSetCommand } from './configs/configs-set.command'; + +@SubCommand({ + name: 'configs', + description: 'Get cortex configurations', + arguments: '', + subCommands: [ConfigsGetCommand, ConfigsListCommand, ConfigsSetCommand], + argsDescription: { + name: 'Configuration name to get', + }, +}) +@SetCommandContext() +export class ConfigsCommand extends CommandRunner { + constructor(readonly contextService: ContextService) { + super(); + } + + async run(): Promise {} +} diff --git a/cortex-js/src/infrastructure/commanders/configs/configs-get.command.ts b/cortex-js/src/infrastructure/commanders/configs/configs-get.command.ts new file mode 100644 index 000000000..8ad60545b --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/configs/configs-get.command.ts @@ -0,0 +1,28 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from '../decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +@SubCommand({ + name: 'get', + description: 'Get a cortex configuration', + arguments: '', + argsDescription: { + name: 'Configuration name to get', + }, +}) +@SetCommandContext() +export class ConfigsGetCommand extends CommandRunner { + constructor( + private readonly configsUsecases: ConfigsUsecases, + readonly contextService: ContextService, + ) { + super(); + } + + async run(passedParams: string[]): Promise { + return this.configsUsecases + .getGroupConfigs(passedParams[0]) + .then(console.table); + } +} diff --git a/cortex-js/src/infrastructure/commanders/configs/configs-list.command.ts b/cortex-js/src/infrastructure/commanders/configs/configs-list.command.ts new file mode 100644 index 000000000..3980412a2 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/configs/configs-list.command.ts @@ -0,0 +1,22 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from '../decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +@SubCommand({ + name: 'list', + description: 'Get all cortex configurations', +}) +@SetCommandContext() +export class ConfigsListCommand extends CommandRunner { + constructor( + private readonly configsUsecases: ConfigsUsecases, + readonly contextService: ContextService, + ) { + super(); + } + + async run(): Promise { + return this.configsUsecases.getConfigs().then(console.table); + } +} diff --git a/cortex-js/src/infrastructure/commanders/configs/configs-set.command.ts b/cortex-js/src/infrastructure/commanders/configs/configs-set.command.ts new file mode 100644 index 000000000..1e5d46327 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/configs/configs-set.command.ts @@ -0,0 +1,54 @@ +import { CommandRunner, SubCommand, Option } from 'nest-commander'; +import { SetCommandContext } from '../decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; + +interface ConfigsSetOption { + key: string; + value: string; + group?: string; +} + +@SubCommand({ + name: 'set', + description: 'Set a cortex configuration', +}) +@SetCommandContext() +export class ConfigsSetCommand extends CommandRunner { + constructor( + private readonly configsUsecases: ConfigsUsecases, + readonly contextService: ContextService, + ) { + super(); + } + + async run(passedParams: string[], options: ConfigsSetOption): Promise { + return this.configsUsecases + .saveConfig(options.key, options.value, options.group) + .then(() => console.log('Set configuration successfully')); + } + + @Option({ + flags: '-k, --key ', + description: 'Configuration key', + }) + parseKey(value: string) { + return value; + } + + @Option({ + flags: '-v, --value ', + description: 'Configuration value', + }) + parseValue(value: string) { + return value; + } + + @Option({ + flags: '-g, --group ', + description: 'Configuration group', + }) + parseGroup(value?: string) { + return value; + } +} diff --git a/cortex-js/src/infrastructure/commanders/cortex-command.commander.ts b/cortex-js/src/infrastructure/commanders/cortex-command.commander.ts index 167e5a048..9ba0d23ed 100644 --- a/cortex-js/src/infrastructure/commanders/cortex-command.commander.ts +++ b/cortex-js/src/infrastructure/commanders/cortex-command.commander.ts @@ -16,6 +16,8 @@ import { BenchmarkCommand } from './benchmark.command'; import chalk from 'chalk'; import { printSlogan } from '@/utils/logo'; import { ContextService } from '../services/context/context.service'; +import { EnginesCommand } from './engines.command'; +import { ConfigsCommand } from './configs.command'; @RootCommand({ subCommands: [ @@ -31,6 +33,8 @@ import { ContextService } from '../services/context/context.service'; TelemetryCommand, EmbeddingCommand, BenchmarkCommand, + EnginesCommand, + ConfigsCommand, ], description: 'Cortex CLI', }) diff --git a/cortex-js/src/infrastructure/commanders/engines.command.ts b/cortex-js/src/infrastructure/commanders/engines.command.ts new file mode 100644 index 000000000..3fc597803 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/engines.command.ts @@ -0,0 +1,20 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from './decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { EnginesUsecases } from '@/usecases/engines/engines.usecase'; +import { EnginesListCommand } from './engines/engines-list.command'; +import { EnginesGetCommand } from './engines/engines-get.command'; + +@SubCommand({ + name: 'engines', + subCommands: [EnginesListCommand, EnginesGetCommand], + description: 'Get cortex engines', +}) +@SetCommandContext() +export class EnginesCommand extends CommandRunner { + constructor(readonly contextService: ContextService) { + super(); + } + + async run(): Promise {} +} diff --git a/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts b/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts new file mode 100644 index 000000000..777219287 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts @@ -0,0 +1,35 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from '../decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { EnginesUsecases } from '@/usecases/engines/engines.usecase'; + +@SubCommand({ + name: 'get', + description: 'Get an engine', + arguments: '', + argsDescription: { + name: 'Engine name to get', + }, +}) +@SetCommandContext() +export class EnginesGetCommand extends CommandRunner { + constructor( + private readonly engineUsecases: EnginesUsecases, + readonly contextService: ContextService, + ) { + super(); + } + + async run(passedParams: string[]): Promise { + return this.engineUsecases.getEngine(passedParams[0]).then((e) => + console.table({ + engine: { + name: passedParams[0], + displayName: e.engine?.name, + description: e.engine?.description, + }, + configs: e.configs, + }), + ); + } +} diff --git a/cortex-js/src/infrastructure/commanders/engines/engines-list.command.ts b/cortex-js/src/infrastructure/commanders/engines/engines-list.command.ts new file mode 100644 index 000000000..4e931bfb7 --- /dev/null +++ b/cortex-js/src/infrastructure/commanders/engines/engines-list.command.ts @@ -0,0 +1,22 @@ +import { CommandRunner, SubCommand } from 'nest-commander'; +import { SetCommandContext } from '../decorators/CommandContext'; +import { ContextService } from '@/infrastructure/services/context/context.service'; +import { EnginesUsecases } from '@/usecases/engines/engines.usecase'; + +@SubCommand({ + name: 'list', + description: 'Get all cortex engines', +}) +@SetCommandContext() +export class EnginesListCommand extends CommandRunner { + constructor( + private readonly enginesUsecases: EnginesUsecases, + readonly contextService: ContextService, + ) { + super(); + } + + async run(): Promise { + return this.enginesUsecases.getEngines().then(console.table); + } +} diff --git a/cortex-js/src/infrastructure/commanders/types/engine.interface.ts b/cortex-js/src/infrastructure/commanders/types/engine.interface.ts index b033be369..91a08d919 100644 --- a/cortex-js/src/infrastructure/commanders/types/engine.interface.ts +++ b/cortex-js/src/infrastructure/commanders/types/engine.interface.ts @@ -2,4 +2,9 @@ export enum Engines { llamaCPP = 'cortex.llamacpp', onnx = 'cortex.onnx', tensorrtLLM = 'cortex.tensorrt-llm', + + // Remote engines + groq = 'groq', + mistral = 'mistral', + openai = 'openai', } diff --git a/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts b/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts index 7e2dc8618..2fda8578f 100644 --- a/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts +++ b/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts @@ -17,6 +17,9 @@ import { FileManagerService } from '@/infrastructure/services/file-manager/file- export default class CortexProvider extends OAIEngineExtension { provider: string = 'cortex'; apiUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/chat_completion`; + name = 'Cortex Inference Engine'; + description? = + 'This extension enables chat completion API calls using the Cortex engine'; private loadModelUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/loadmodel`; private unloadModelUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/unloadmodel`; diff --git a/cortex-js/src/infrastructure/repositories/extensions/extension.module.ts b/cortex-js/src/infrastructure/repositories/extensions/extension.module.ts index 236e8ce78..0c29d756c 100644 --- a/cortex-js/src/infrastructure/repositories/extensions/extension.module.ts +++ b/cortex-js/src/infrastructure/repositories/extensions/extension.module.ts @@ -4,9 +4,15 @@ import { ExtensionRepository } from '@/domain/repositories/extension.interface'; import { CortexProviderModule } from '@/infrastructure/providers/cortex/cortex.module'; import { HttpModule } from '@nestjs/axios'; import { FileManagerModule } from '@/infrastructure/services/file-manager/file-manager.module'; +import { ExtensionsModule } from '@/extensions/extensions.module'; @Module({ - imports: [CortexProviderModule, HttpModule, FileManagerModule], + imports: [ + CortexProviderModule, + HttpModule, + FileManagerModule, + ExtensionsModule, + ], providers: [ { provide: ExtensionRepository, diff --git a/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts b/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts index f9bc3d809..2c69bfbd1 100644 --- a/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts +++ b/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts @@ -4,10 +4,10 @@ import { Extension } from '@/domain/abstracts/extension.abstract'; import { readdir, lstat } from 'fs/promises'; import { join } from 'path'; import { EngineExtension } from '@/domain/abstracts/engine.abstract'; -import { appPath } from '@/utils/app-path'; import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service'; import { existsSync } from 'fs'; import { Engines } from '@/infrastructure/commanders/types/engine.interface'; +import { OAIEngineExtension } from '@/domain/abstracts/oai.abstract'; @Injectable() export class ExtensionRepositoryImpl implements ExtensionRepository { @@ -18,10 +18,9 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { @Inject('CORTEX_PROVIDER') private readonly cortexProvider: EngineExtension, private readonly fileService: FileManagerService, + @Inject('EXTENSIONS_PROVIDER') + private readonly coreExtensions: OAIEngineExtension[], ) { - this.extensions.set(Engines.llamaCPP, this.cortexProvider); - this.extensions.set(Engines.onnx, this.cortexProvider); - this.extensions.set(Engines.tensorrtLLM, this.cortexProvider); this.loadCoreExtensions(); this.loadExternalExtensions(); } @@ -30,7 +29,14 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { return Promise.resolve(object); } findAll(): Promise { - return Promise.resolve(Array.from(this.extensions.values())); + return Promise.resolve( + Array.from(this.extensions.keys()).map( + (e) => + ({ + name: e, + }) as Extension, + ), + ); } findOne(id: string): Promise { return Promise.resolve(this.extensions.get(id) ?? null); @@ -43,9 +49,15 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { return Promise.resolve(); } - private loadCoreExtensions(): void { - const extensionsPath = join(appPath, 'src', 'extensions'); - this.loadExtensions(extensionsPath); + private async loadCoreExtensions() { + await this.cortexProvider.onLoad(); + this.extensions.set(Engines.llamaCPP, this.cortexProvider); + this.extensions.set(Engines.onnx, this.cortexProvider); + this.extensions.set(Engines.tensorrtLLM, this.cortexProvider); + for (const extension of this.coreExtensions) { + await extension.onLoad(); + this.extensions.set(extension.provider, extension); + } } private async loadExternalExtensions() { diff --git a/cortex-js/src/infrastructure/services/file-manager/file-manager.service.ts b/cortex-js/src/infrastructure/services/file-manager/file-manager.service.ts index 4ac66f089..e1ea41e92 100644 --- a/cortex-js/src/infrastructure/services/file-manager/file-manager.service.ts +++ b/cortex-js/src/infrastructure/services/file-manager/file-manager.service.ts @@ -38,7 +38,7 @@ export class FileManagerService { * Get cortex configs * @returns the config object */ - async getConfig(): Promise { + async getConfig(): Promise { const homeDir = os.homedir(); const configPath = join(homeDir, this.configFile); @@ -66,7 +66,7 @@ export class FileManagerService { } } - async writeConfigFile(config: Config): Promise { + async writeConfigFile(config: Config & object): Promise { const homeDir = os.homedir(); const configPath = join(homeDir, this.configFile); diff --git a/cortex-js/src/usecases/configs/configs.module.ts b/cortex-js/src/usecases/configs/configs.module.ts new file mode 100644 index 000000000..afa4bca2f --- /dev/null +++ b/cortex-js/src/usecases/configs/configs.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { FileManagerModule } from '@/infrastructure/services/file-manager/file-manager.module'; +import { ConfigsUsecases } from './configs.usecase'; + +@Module({ + imports: [FileManagerModule], + controllers: [], + providers: [ConfigsUsecases], + exports: [ConfigsUsecases], +}) +export class ConfigsModule {} diff --git a/cortex-js/src/usecases/configs/configs.usecase.ts b/cortex-js/src/usecases/configs/configs.usecase.ts new file mode 100644 index 000000000..3faf2b720 --- /dev/null +++ b/cortex-js/src/usecases/configs/configs.usecase.ts @@ -0,0 +1,63 @@ +import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ConfigsUsecases { + constructor(private readonly fileManagerService: FileManagerService) {} + + /** + * Save a configuration to the .cortexrc file. + * @param key Configuration Key + * @param group Configuration Group where the key belongs + */ + async saveConfig(key: string, value: string, group?: string) { + const configs = await this.fileManagerService.getConfig(); + + const groupConfigs = configs[ + group as keyof typeof configs + ] as unknown as object; + const newConfigs = { + ...configs, + ...(group + ? { + [group]: { + ...groupConfigs, + [key]: value, + }, + } + : {}), + }; + + await this.fileManagerService.writeConfigFile(newConfigs); + } + + /** + * Get the configurations of a group. + * @param group + * @returns + */ + async getGroupConfigs(group: string) { + const configs = await this.fileManagerService.getConfig(); + return configs[group as keyof typeof configs] as unknown as object; + } + + /** + * Get all available configurations. + * @param group + * @returns + */ + async getConfigs() { + return this.fileManagerService.getConfig(); + } + + /** + * Get the configuration with given key + * @param group + * @param key + * @returns + */ + async getKeyConfig(key: string) { + const configs = await this.fileManagerService.getConfig(); + return configs[key as keyof typeof configs]; + } +} diff --git a/cortex-js/src/usecases/engines/engines.module.ts b/cortex-js/src/usecases/engines/engines.module.ts new file mode 100644 index 000000000..40f15df8c --- /dev/null +++ b/cortex-js/src/usecases/engines/engines.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigsModule } from '../configs/configs.module'; +import { EnginesUsecases } from './engines.usecase'; +import { ExtensionModule } from '@/infrastructure/repositories/extensions/extension.module'; + +@Module({ + imports: [ConfigsModule, ExtensionModule], + controllers: [], + providers: [EnginesUsecases], + exports: [EnginesUsecases], +}) +export class EnginesModule {} diff --git a/cortex-js/src/usecases/engines/engines.usecase.ts b/cortex-js/src/usecases/engines/engines.usecase.ts new file mode 100644 index 000000000..7a4293a25 --- /dev/null +++ b/cortex-js/src/usecases/engines/engines.usecase.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigsUsecases } from '../configs/configs.usecase'; +import { ExtensionRepository } from '@/domain/repositories/extension.interface'; + +@Injectable() +export class EnginesUsecases { + constructor( + private readonly configsUsechases: ConfigsUsecases, + private readonly extensionRepository: ExtensionRepository, + ) {} + + /** + * Get the engines + * @returns Cortex supported Engines + */ + async getEngines() { + return this.extensionRepository.findAll(); + } + + /** + * Get the engine with the given name + * @param name Engine name + * @returns Engine + */ + async getEngine(name: string) { + return { + engine: await this.extensionRepository.findOne(name), + configs: await this.configsUsechases.getGroupConfigs(name), + }; + } +} From 161d70e857920bd24e90e18ac5c47e4269b7ffde Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 21:32:04 +0700 Subject: [PATCH 2/6] chore: add API endpoints --- cortex-js/src/app.module.ts | 4 + .../src/domain/abstracts/engine.abstract.ts | 2 - .../domain/abstracts/extension.abstract.ts | 2 +- cortex-js/src/extensions/groq.engine.ts | 9 ++- cortex-js/src/extensions/mistral.engine.ts | 9 ++- cortex-js/src/extensions/openai.engine.ts | 11 +-- .../commanders/engines/engines-get.command.ts | 11 +-- .../controllers/configs.controller.spec.ts | 41 ++++++++++ .../controllers/configs.controller.ts | 81 +++++++++++++++++++ .../controllers/engines.controller.spec.ts | 41 ++++++++++ .../controllers/engines.controller.ts | 55 +++++++++++++ .../dtos/common/common-response.dto.ts | 10 +++ .../dtos/configs/config-update.dto.ts | 31 +++++++ .../dtos/engines/engines.dto.ts | 42 ++++++++++ .../providers/cortex/cortex.provider.ts | 7 +- .../extensions/extension.repository.ts | 47 +++++++---- .../src/usecases/configs/configs.usecase.ts | 13 ++- .../src/usecases/engines/engines.usecase.ts | 27 ++++--- 18 files changed, 386 insertions(+), 57 deletions(-) create mode 100644 cortex-js/src/infrastructure/controllers/configs.controller.spec.ts create mode 100644 cortex-js/src/infrastructure/controllers/configs.controller.ts create mode 100644 cortex-js/src/infrastructure/controllers/engines.controller.spec.ts create mode 100644 cortex-js/src/infrastructure/controllers/engines.controller.ts create mode 100644 cortex-js/src/infrastructure/dtos/common/common-response.dto.ts create mode 100644 cortex-js/src/infrastructure/dtos/configs/config-update.dto.ts create mode 100644 cortex-js/src/infrastructure/dtos/engines/engines.dto.ts diff --git a/cortex-js/src/app.module.ts b/cortex-js/src/app.module.ts index 270c9f110..a343e4260 100644 --- a/cortex-js/src/app.module.ts +++ b/cortex-js/src/app.module.ts @@ -29,6 +29,8 @@ import { ContextModule } from './infrastructure/services/context/context.module' import { ExtensionsModule } from './extensions/extensions.module'; import { ConfigsModule } from './usecases/configs/configs.module'; import { EnginesModule } from './usecases/engines/engines.module'; +import { ConfigsController } from './infrastructure/controllers/configs.controller'; +import { EnginesController } from './infrastructure/controllers/engines.controller'; @Module({ imports: [ @@ -66,6 +68,8 @@ import { EnginesModule } from './usecases/engines/engines.module'; StatusController, ProcessController, EventsController, + ConfigsController, + EnginesController, ], providers: [ { diff --git a/cortex-js/src/domain/abstracts/engine.abstract.ts b/cortex-js/src/domain/abstracts/engine.abstract.ts index 1e11352bc..d203218fa 100644 --- a/cortex-js/src/domain/abstracts/engine.abstract.ts +++ b/cortex-js/src/domain/abstracts/engine.abstract.ts @@ -4,8 +4,6 @@ import { Model, ModelSettingParams } from '../../domain/models/model.interface'; import { Extension } from './extension.abstract'; export abstract class EngineExtension extends Extension { - abstract provider: string; - abstract onLoad(): void; abstract inference( diff --git a/cortex-js/src/domain/abstracts/extension.abstract.ts b/cortex-js/src/domain/abstracts/extension.abstract.ts index cd1298718..d016920ed 100644 --- a/cortex-js/src/domain/abstracts/extension.abstract.ts +++ b/cortex-js/src/domain/abstracts/extension.abstract.ts @@ -4,7 +4,7 @@ */ export abstract class Extension { /** @type {string} Name of the extension. */ - name?: string; + name: string; /** @type {string} Product Name of the extension. */ productName?: string; diff --git a/cortex-js/src/extensions/groq.engine.ts b/cortex-js/src/extensions/groq.engine.ts index 276d64a5c..046589547 100644 --- a/cortex-js/src/extensions/groq.engine.ts +++ b/cortex-js/src/extensions/groq.engine.ts @@ -8,10 +8,11 @@ import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ export default class GroqEngineExtension extends OAIEngineExtension { - provider: string = 'groq'; apiUrl = 'https://api.groq.com/openai/v1/chat/completions'; - name = 'Groq Inference Engine'; + name = 'groq'; + productName = 'Groq Inference Engine'; description = 'This extension enables fast Groq chat completion API calls'; + version = '0.0.1'; constructor( protected readonly httpService: HttpService, @@ -22,9 +23,9 @@ export default class GroqEngineExtension extends OAIEngineExtension { async onLoad() { const configs = (await this.configsUsecases.getGroupConfigs( - this.provider, + this.name, )) as unknown as { apiKey: string }; if (!configs?.apiKey) - await this.configsUsecases.saveConfig('apiKey', '', this.provider); + await this.configsUsecases.saveConfig('apiKey', '', this.name); } } diff --git a/cortex-js/src/extensions/mistral.engine.ts b/cortex-js/src/extensions/mistral.engine.ts index 0c3445f1f..327176f86 100644 --- a/cortex-js/src/extensions/mistral.engine.ts +++ b/cortex-js/src/extensions/mistral.engine.ts @@ -8,10 +8,11 @@ import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ export default class MistralEngineExtension extends OAIEngineExtension { - provider: string = 'mistral'; apiUrl = 'https://api.mistral.ai/v1/chat/completions'; - name = 'Mistral Inference Engine'; + name = 'mistral'; + productName = 'Mistral Inference Engine'; description = 'This extension enables Mistral chat completion API calls'; + version = '0.0.1'; constructor( protected readonly httpService: HttpService, @@ -22,9 +23,9 @@ export default class MistralEngineExtension extends OAIEngineExtension { async onLoad() { const configs = (await this.configsUsecases.getGroupConfigs( - this.provider, + this.name, )) as unknown as { apiKey: string }; if (!configs?.apiKey) - await this.configsUsecases.saveConfig('apiKey', '', this.provider); + await this.configsUsecases.saveConfig('apiKey', '', this.name); } } diff --git a/cortex-js/src/extensions/openai.engine.ts b/cortex-js/src/extensions/openai.engine.ts index d38ada6cf..6022fe91f 100644 --- a/cortex-js/src/extensions/openai.engine.ts +++ b/cortex-js/src/extensions/openai.engine.ts @@ -8,10 +8,11 @@ import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ export default class OpenAIEngineExtension extends OAIEngineExtension { - provider: string = 'openai'; apiUrl = 'https://api.openai.com/v1/chat/completions'; - name = 'OpenAI Inference Engine'; - description? = 'This extension enables OpenAI chat completion API calls'; + name = 'openai'; + productName = 'OpenAI Inference Engine'; + description = 'This extension enables OpenAI chat completion API calls'; + version = '0.0.1'; constructor( protected readonly httpService: HttpService, @@ -22,9 +23,9 @@ export default class OpenAIEngineExtension extends OAIEngineExtension { async onLoad() { const configs = (await this.configsUsecases.getGroupConfigs( - this.provider, + this.name, )) as unknown as { apiKey: string }; if (!configs?.apiKey) - await this.configsUsecases.saveConfig('apiKey', '', this.provider); + await this.configsUsecases.saveConfig('apiKey', '', this.name); } } diff --git a/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts b/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts index 777219287..8eefc6f44 100644 --- a/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts +++ b/cortex-js/src/infrastructure/commanders/engines/engines-get.command.ts @@ -21,15 +21,6 @@ export class EnginesGetCommand extends CommandRunner { } async run(passedParams: string[]): Promise { - return this.engineUsecases.getEngine(passedParams[0]).then((e) => - console.table({ - engine: { - name: passedParams[0], - displayName: e.engine?.name, - description: e.engine?.description, - }, - configs: e.configs, - }), - ); + return this.engineUsecases.getEngine(passedParams[0]).then(console.table); } } diff --git a/cortex-js/src/infrastructure/controllers/configs.controller.spec.ts b/cortex-js/src/infrastructure/controllers/configs.controller.spec.ts new file mode 100644 index 000000000..c788da2ae --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/configs.controller.spec.ts @@ -0,0 +1,41 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DatabaseModule } from '../database/database.module'; +import { ExtensionModule } from '../repositories/extensions/extension.module'; +import { ModelRepositoryModule } from '../repositories/models/model.module'; +import { HttpModule } from '@nestjs/axios'; +import { DownloadManagerModule } from '@/infrastructure/services/download-manager/download-manager.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { TelemetryModule } from '@/usecases/telemetry/telemetry.module'; +import { FileManagerModule } from '../services/file-manager/file-manager.module'; +import { ConfigsController } from './configs.controller'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; +import { ConfigsModule } from '@/usecases/configs/configs.module'; + +describe('ConfigsController', () => { + let controller: ConfigsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + EventEmitterModule.forRoot(), + DatabaseModule, + ExtensionModule, + ModelRepositoryModule, + HttpModule, + DownloadManagerModule, + EventEmitterModule.forRoot(), + TelemetryModule, + FileManagerModule, + ConfigsModule, + ], + controllers: [ConfigsController], + providers: [ConfigsUsecases], + }).compile(); + + controller = module.get(ConfigsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/cortex-js/src/infrastructure/controllers/configs.controller.ts b/cortex-js/src/infrastructure/controllers/configs.controller.ts new file mode 100644 index 000000000..f739fd7a0 --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/configs.controller.ts @@ -0,0 +1,81 @@ +import { + Controller, + Get, + Post, + Body, + Param, + HttpCode, + UseInterceptors, +} from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags, ApiResponse } from '@nestjs/swagger'; +import { TransformInterceptor } from '../interceptors/transform.interceptor'; +import { ConfigsUsecases } from '@/usecases/configs/configs.usecase'; +import { ConfigUpdateDto } from '../dtos/configs/config-update.dto'; +import { CommonResponseDto } from '../dtos/common/common-response.dto'; + +@ApiTags('Configurations') +@Controller('configs') +@UseInterceptors(TransformInterceptor) +export class ConfigsController { + constructor(private readonly configsUsecases: ConfigsUsecases) {} + + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'Ok', + type: [Object], + }) + @ApiOperation({ + summary: 'List configs', + description: + 'Lists the currently available configs, including the default and user-defined configurations', + }) + @Get() + findAll() { + return this.configsUsecases.getConfigs(); + } + + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'Ok', + type: Object, + }) + @ApiOperation({ + summary: 'Get a config', + description: + 'Retrieves a config instance, providing basic information about the config', + }) + @ApiParam({ + name: 'name', + required: true, + description: 'The unique identifier of the config.', + }) + @Get(':name(*)') + findOne(@Param('name') name: string) { + return this.configsUsecases.getGroupConfigs(name); + } + + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'The config has been successfully updated.', + type: CommonResponseDto, + }) + @ApiOperation({ + summary: 'Configure a model', + description: "Updates a config by it's group and key", + parameters: [ + { + in: 'path', + name: 'model', + required: true, + description: 'The unique identifier of the model.', + }, + ], + }) + @Post(':name(*)') + async update(@Param('name') name: string, @Body() configs: ConfigUpdateDto) { + return this.configsUsecases.saveConfig(configs.key, configs.value, name); + } +} diff --git a/cortex-js/src/infrastructure/controllers/engines.controller.spec.ts b/cortex-js/src/infrastructure/controllers/engines.controller.spec.ts new file mode 100644 index 000000000..2f488f71b --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/engines.controller.spec.ts @@ -0,0 +1,41 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DatabaseModule } from '../database/database.module'; +import { ExtensionModule } from '../repositories/extensions/extension.module'; +import { ModelRepositoryModule } from '../repositories/models/model.module'; +import { HttpModule } from '@nestjs/axios'; +import { DownloadManagerModule } from '@/infrastructure/services/download-manager/download-manager.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { TelemetryModule } from '@/usecases/telemetry/telemetry.module'; +import { FileManagerModule } from '../services/file-manager/file-manager.module'; +import { EnginesController } from './engines.controller'; +import { EnginesUsecases } from '@/usecases/engines/engines.usecase'; +import { EnginesModule } from '@/usecases/engines/engines.module'; + +describe('ConfigsController', () => { + let controller: EnginesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + EventEmitterModule.forRoot(), + DatabaseModule, + ExtensionModule, + ModelRepositoryModule, + HttpModule, + DownloadManagerModule, + EventEmitterModule.forRoot(), + TelemetryModule, + FileManagerModule, + EnginesModule, + ], + controllers: [EnginesController], + providers: [EnginesUsecases], + }).compile(); + + controller = module.get(EnginesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/cortex-js/src/infrastructure/controllers/engines.controller.ts b/cortex-js/src/infrastructure/controllers/engines.controller.ts new file mode 100644 index 000000000..5d3606fe5 --- /dev/null +++ b/cortex-js/src/infrastructure/controllers/engines.controller.ts @@ -0,0 +1,55 @@ +import { + Controller, + Get, + Param, + HttpCode, + UseInterceptors, +} from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags, ApiResponse } from '@nestjs/swagger'; +import { TransformInterceptor } from '../interceptors/transform.interceptor'; +import { EnginesUsecases } from '@/usecases/engines/engines.usecase'; +import { EngineDto } from '../dtos/engines/engines.dto'; + +@ApiTags('Engines') +@Controller('engines') +@UseInterceptors(TransformInterceptor) +export class EnginesController { + constructor(private readonly enginesUsecases: EnginesUsecases) {} + + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'Ok', + type: [EngineDto], + }) + @ApiOperation({ + summary: 'List available engines', + description: + 'Lists the currently available engines, including local and remote engines', + }) + @Get() + findAll() { + return this.enginesUsecases.getEngines(); + } + + @HttpCode(200) + @ApiResponse({ + status: 200, + description: 'Ok', + type: EngineDto, + }) + @ApiOperation({ + summary: 'Get an engine', + description: + 'Retrieves an engine instance, providing basic information about the engine', + }) + @ApiParam({ + name: 'name', + required: true, + description: 'The unique identifier of the engine.', + }) + @Get(':name(*)') + findOne(@Param('name') name: string) { + return this.enginesUsecases.getEngine(name); + } +} diff --git a/cortex-js/src/infrastructure/dtos/common/common-response.dto.ts b/cortex-js/src/infrastructure/dtos/common/common-response.dto.ts new file mode 100644 index 000000000..a98d1b56f --- /dev/null +++ b/cortex-js/src/infrastructure/dtos/common/common-response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class CommonResponseDto { + @ApiProperty({ + description: 'The success or error message', + }) + @IsString() + message: string; +} diff --git a/cortex-js/src/infrastructure/dtos/configs/config-update.dto.ts b/cortex-js/src/infrastructure/dtos/configs/config-update.dto.ts new file mode 100644 index 000000000..3556aa7cd --- /dev/null +++ b/cortex-js/src/infrastructure/dtos/configs/config-update.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class ConfigUpdateDto { + @ApiProperty({ + example: 'apiKey', + description: 'The configuration key.', + }) + @IsString() + @IsOptional() + key: string; + + // Prompt Settings + @ApiProperty({ + type: String, + example: 'sk-xxxxxx', + description: 'The value of the configuration.', + }) + @IsString() + @IsOptional() + value: string; + + @ApiProperty({ + type: String, + example: 'openai', + description: 'The configuration name.', + }) + @IsString() + @IsOptional() + name?: string; +} diff --git a/cortex-js/src/infrastructure/dtos/engines/engines.dto.ts b/cortex-js/src/infrastructure/dtos/engines/engines.dto.ts new file mode 100644 index 000000000..d222c15d1 --- /dev/null +++ b/cortex-js/src/infrastructure/dtos/engines/engines.dto.ts @@ -0,0 +1,42 @@ +import { Extension } from '@/domain/abstracts/extension.abstract'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; + +export class EngineDto implements Partial { + @ApiProperty({ + type: String, + example: 'cortex.llamacpp', + description: + 'The name of the engine, which can be referenced in the API endpoints.', + }) + @IsString() + name: string; + + // Prompt Settings + @ApiProperty({ + type: String, + example: 'Cortex', + description: 'The display name of the engine', + }) + @IsString() + @IsOptional() + productName?: string; + + @ApiProperty({ + type: String, + example: 'Cortex engine', + description: 'The description of the engine', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiProperty({ + type: String, + example: '0.0.1', + description: 'The version of the engine', + }) + @IsString() + @IsOptional() + version?: string; +} diff --git a/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts b/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts index 2fda8578f..3884eea27 100644 --- a/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts +++ b/cortex-js/src/infrastructure/providers/cortex/cortex.provider.ts @@ -15,11 +15,12 @@ import { FileManagerService } from '@/infrastructure/services/file-manager/file- @Injectable() export default class CortexProvider extends OAIEngineExtension { - provider: string = 'cortex'; apiUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/chat_completion`; - name = 'Cortex Inference Engine'; - description? = + name = 'cortex'; + productName = 'Cortex Inference Engine'; + description = 'This extension enables chat completion API calls using the Cortex engine'; + version = '0.0.1'; private loadModelUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/loadmodel`; private unloadModelUrl = `http://${defaultCortexCppHost}:${defaultCortexCppPort}/inferences/server/unloadmodel`; diff --git a/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts b/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts index 2c69bfbd1..b37ece430 100644 --- a/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts +++ b/cortex-js/src/infrastructure/repositories/extensions/extension.repository.ts @@ -3,11 +3,12 @@ import { ExtensionRepository } from '@/domain/repositories/extension.interface'; import { Extension } from '@/domain/abstracts/extension.abstract'; import { readdir, lstat } from 'fs/promises'; import { join } from 'path'; -import { EngineExtension } from '@/domain/abstracts/engine.abstract'; import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service'; import { existsSync } from 'fs'; import { Engines } from '@/infrastructure/commanders/types/engine.interface'; import { OAIEngineExtension } from '@/domain/abstracts/oai.abstract'; +import CortexProvider from '@/infrastructure/providers/cortex/cortex.provider'; +import { HttpService } from '@nestjs/axios'; @Injectable() export class ExtensionRepositoryImpl implements ExtensionRepository { @@ -15,11 +16,11 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { extensions = new Map(); constructor( - @Inject('CORTEX_PROVIDER') - private readonly cortexProvider: EngineExtension, private readonly fileService: FileManagerService, @Inject('EXTENSIONS_PROVIDER') private readonly coreExtensions: OAIEngineExtension[], + private readonly httpService: HttpService, + private readonly fileManagerService: FileManagerService, ) { this.loadCoreExtensions(); this.loadExternalExtensions(); @@ -29,14 +30,7 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { return Promise.resolve(object); } findAll(): Promise { - return Promise.resolve( - Array.from(this.extensions.keys()).map( - (e) => - ({ - name: e, - }) as Extension, - ), - ); + return Promise.resolve(Array.from(this.extensions.values())); } findOne(id: string): Promise { return Promise.resolve(this.extensions.get(id) ?? null); @@ -50,13 +44,34 @@ export class ExtensionRepositoryImpl implements ExtensionRepository { } private async loadCoreExtensions() { - await this.cortexProvider.onLoad(); - this.extensions.set(Engines.llamaCPP, this.cortexProvider); - this.extensions.set(Engines.onnx, this.cortexProvider); - this.extensions.set(Engines.tensorrtLLM, this.cortexProvider); + const llamaCPPEngine = new CortexProvider( + this.httpService, + this.fileManagerService, + ); + llamaCPPEngine.name = 'cortex.llamacpp'; + const onnxEngine = new CortexProvider( + this.httpService, + this.fileManagerService, + ); + onnxEngine.name = 'cortex.onnx'; + + const tensorrtLLMEngine = new CortexProvider( + this.httpService, + this.fileManagerService, + ); + tensorrtLLMEngine.name = 'cortex.tensorrt-llm'; + + await llamaCPPEngine.onLoad(); + await onnxEngine.onLoad(); + await tensorrtLLMEngine.onLoad(); + + this.extensions.set(Engines.llamaCPP, llamaCPPEngine); + this.extensions.set(Engines.onnx, onnxEngine); + this.extensions.set(Engines.tensorrtLLM, tensorrtLLMEngine); + for (const extension of this.coreExtensions) { await extension.onLoad(); - this.extensions.set(extension.provider, extension); + this.extensions.set(extension.name, extension); } } diff --git a/cortex-js/src/usecases/configs/configs.usecase.ts b/cortex-js/src/usecases/configs/configs.usecase.ts index 3faf2b720..f1b47b7ee 100644 --- a/cortex-js/src/usecases/configs/configs.usecase.ts +++ b/cortex-js/src/usecases/configs/configs.usecase.ts @@ -1,3 +1,4 @@ +import { CommonResponseDto } from '@/infrastructure/dtos/common/common-response.dto'; import { FileManagerService } from '@/infrastructure/services/file-manager/file-manager.service'; import { Injectable } from '@nestjs/common'; @@ -10,7 +11,11 @@ export class ConfigsUsecases { * @param key Configuration Key * @param group Configuration Group where the key belongs */ - async saveConfig(key: string, value: string, group?: string) { + async saveConfig( + key: string, + value: string, + group?: string, + ): Promise { const configs = await this.fileManagerService.getConfig(); const groupConfigs = configs[ @@ -28,7 +33,11 @@ export class ConfigsUsecases { : {}), }; - await this.fileManagerService.writeConfigFile(newConfigs); + return this.fileManagerService.writeConfigFile(newConfigs).then(() => { + return { + message: 'The config has been successfully updated.', + }; + }); } /** diff --git a/cortex-js/src/usecases/engines/engines.usecase.ts b/cortex-js/src/usecases/engines/engines.usecase.ts index 7a4293a25..1609822a2 100644 --- a/cortex-js/src/usecases/engines/engines.usecase.ts +++ b/cortex-js/src/usecases/engines/engines.usecase.ts @@ -1,20 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { ConfigsUsecases } from '../configs/configs.usecase'; import { ExtensionRepository } from '@/domain/repositories/extension.interface'; @Injectable() export class EnginesUsecases { - constructor( - private readonly configsUsechases: ConfigsUsecases, - private readonly extensionRepository: ExtensionRepository, - ) {} + constructor(private readonly extensionRepository: ExtensionRepository) {} /** * Get the engines * @returns Cortex supported Engines */ async getEngines() { - return this.extensionRepository.findAll(); + return (await this.extensionRepository.findAll()).map((e) => ({ + name: e.name, + description: e.description, + version: e.version, + productName: e.productName, + })); } /** @@ -23,9 +24,15 @@ export class EnginesUsecases { * @returns Engine */ async getEngine(name: string) { - return { - engine: await this.extensionRepository.findOne(name), - configs: await this.configsUsechases.getGroupConfigs(name), - }; + return this.extensionRepository.findOne(name).then((engine) => + engine + ? { + name: engine.name, + description: engine.description, + version: engine.version, + productName: engine.productName, + } + : undefined, + ); } } From 37f6f1db678d7ea994f116d8a650afef9e5f774e Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 21:43:40 +0700 Subject: [PATCH 3/6] fix: chat/completions is missing content-type header --- cortex-js/src/infrastructure/controllers/chat.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cortex-js/src/infrastructure/controllers/chat.controller.ts b/cortex-js/src/infrastructure/controllers/chat.controller.ts index 01039e9ce..967e68cfa 100644 --- a/cortex-js/src/infrastructure/controllers/chat.controller.ts +++ b/cortex-js/src/infrastructure/controllers/chat.controller.ts @@ -27,11 +27,14 @@ export class ChatController { @Res() res: Response, ) { const { stream } = createChatDto; + if (stream) { + res.header('Content-Type', 'text/event-stream'); this.chatService .inference(createChatDto, headers) .then((stream) => stream.pipe(res)); } else { + res.header('Content-Type', 'application/json'); res.json(await this.chatService.inference(createChatDto, headers)); } } From 185f9b68dd1aee1e4453876f161e1a4477b6b0f5 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 22:00:11 +0700 Subject: [PATCH 4/6] chore: update playground link and global prefix /v1 --- cortex-js/src/app.ts | 5 ++++- cortex-js/src/infrastructure/commanders/serve.command.ts | 7 +++++-- cortex-js/src/main.ts | 7 +++++-- cortex-js/src/usecases/models/models.usecases.ts | 1 - 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cortex-js/src/app.ts b/cortex-js/src/app.ts index ed7da59a6..9eb1d8d70 100644 --- a/cortex-js/src/app.ts +++ b/cortex-js/src/app.ts @@ -7,9 +7,12 @@ export const getApp = async () => { const app = await NestFactory.create(AppModule, { snapshot: true, cors: true, - logger: console + logger: console, }); + // Set the global prefix for the API /v1/ + app.setGlobalPrefix('v1'); + const fileService = app.get(FileManagerService); await fileService.getConfig(); diff --git a/cortex-js/src/infrastructure/commanders/serve.command.ts b/cortex-js/src/infrastructure/commanders/serve.command.ts index a8ade2238..42bfbc6a1 100644 --- a/cortex-js/src/infrastructure/commanders/serve.command.ts +++ b/cortex-js/src/infrastructure/commanders/serve.command.ts @@ -7,7 +7,7 @@ import { SetCommandContext } from './decorators/CommandContext'; import { ServeStopCommand } from './sub-commands/serve-stop.command'; import { ContextService } from '../services/context/context.service'; import { getApp } from '@/app'; -import { Logger } from '@nestjs/common'; +import chalk from 'chalk'; type ServeOptions = { address?: string; @@ -37,7 +37,10 @@ export class ServeCommand extends CommandRunner { const app = await getApp(); await app.listen(port, host); - console.log(`Started server at http://${host}:${port}`); + console.log(chalk.blue(`Started server at http://${host}:${port}`)); + console.log( + chalk.blue(`API Playground available at http://${host}:${port}/api`), + ); } @Option({ diff --git a/cortex-js/src/main.ts b/cortex-js/src/main.ts index 804e9481e..217b84884 100644 --- a/cortex-js/src/main.ts +++ b/cortex-js/src/main.ts @@ -3,6 +3,7 @@ import { defaultCortexJsPort, } from '@/infrastructure/constants/cortex'; import { getApp } from './app'; +import chalk from 'chalk'; async function bootstrap() { const app = await getApp(); @@ -11,8 +12,10 @@ async function bootstrap() { const port = process.env.CORTEX_JS_PORT || defaultCortexJsPort; await app.listen(port, host); - console.log(`Started server at http://${host}:${port}`); - console.log(`Swagger UI available at http://${host}:${port}/api`); + console.log(chalk.blue(`Started server at http://${host}:${port}`)); + console.log( + chalk.blue(`API Playground available at http://${host}:${port}/api`), + ); } bootstrap(); diff --git a/cortex-js/src/usecases/models/models.usecases.ts b/cortex-js/src/usecases/models/models.usecases.ts index a1615b970..30afc5815 100644 --- a/cortex-js/src/usecases/models/models.usecases.ts +++ b/cortex-js/src/usecases/models/models.usecases.ts @@ -224,7 +224,6 @@ export class ModelsUsecases { metadata: {}, }; this.eventEmitter.emit('model.event', modelEvent); - console.error('Starting model failed', e.code, e.message, e.stack); if (e.code === AxiosError.ERR_BAD_REQUEST) { return { message: 'Model already loaded', From 4d60260d3c2034605d8048ee7ec2ccebd746b427 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 22:10:27 +0700 Subject: [PATCH 5/6] chore: cortex serve error handling - port conflict --- .../src/infrastructure/commanders/serve.command.ts | 14 +++++++++----- cortex-js/src/main.ts | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cortex-js/src/infrastructure/commanders/serve.command.ts b/cortex-js/src/infrastructure/commanders/serve.command.ts index 42bfbc6a1..468fc4de3 100644 --- a/cortex-js/src/infrastructure/commanders/serve.command.ts +++ b/cortex-js/src/infrastructure/commanders/serve.command.ts @@ -36,11 +36,15 @@ export class ServeCommand extends CommandRunner { private async startServer(host: string, port: number) { const app = await getApp(); - await app.listen(port, host); - console.log(chalk.blue(`Started server at http://${host}:${port}`)); - console.log( - chalk.blue(`API Playground available at http://${host}:${port}/api`), - ); + try { + await app.listen(port, host); + console.log(chalk.blue(`Started server at http://${host}:${port}`)); + console.log( + chalk.blue(`API Playground available at http://${host}:${port}/api`), + ); + } catch (err) { + console.error(err.message); + } } @Option({ diff --git a/cortex-js/src/main.ts b/cortex-js/src/main.ts index 217b84884..e54d8f10e 100644 --- a/cortex-js/src/main.ts +++ b/cortex-js/src/main.ts @@ -11,11 +11,15 @@ async function bootstrap() { const host = process.env.CORTEX_JS_HOST || defaultCortexJsHost; const port = process.env.CORTEX_JS_PORT || defaultCortexJsPort; - await app.listen(port, host); - console.log(chalk.blue(`Started server at http://${host}:${port}`)); - console.log( - chalk.blue(`API Playground available at http://${host}:${port}/api`), - ); + try { + await app.listen(port, host); + console.log(chalk.blue(`Started server at http://${host}:${port}`)); + console.log( + chalk.blue(`API Playground available at http://${host}:${port}/api`), + ); + } catch (err) { + console.error(err.message); + } } bootstrap(); From 90329653bedd356139bf6fe613ade90a3781bbf1 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 26 Jun 2024 22:13:56 +0700 Subject: [PATCH 6/6] chore: error handling - start stop model with error message --- cortex-js/src/usecases/models/models.usecases.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cortex-js/src/usecases/models/models.usecases.ts b/cortex-js/src/usecases/models/models.usecases.ts index 30afc5815..4a1c66e95 100644 --- a/cortex-js/src/usecases/models/models.usecases.ts +++ b/cortex-js/src/usecases/models/models.usecases.ts @@ -235,7 +235,7 @@ export class ModelsUsecases { TelemetrySource.CORTEX_CPP, ); return { - message: 'Failed to load model', + message: e.message, modelId, }; }); @@ -298,7 +298,7 @@ export class ModelsUsecases { TelemetrySource.CORTEX_CPP, ); return { - message: 'Failed to stop model', + message: e.message, modelId, }; });