Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit b50322d

Browse files
committed
feat: add model event
Signed-off-by: James <namnh0122@gmail.com>
1 parent 40ada07 commit b50322d

File tree

6 files changed

+159
-15
lines changed

6 files changed

+159
-15
lines changed

cortex-js/src/app.module.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { AppLoggerMiddleware } from './infrastructure/middlewares/app.logger.mid
1717
import { EventEmitterModule } from '@nestjs/event-emitter';
1818
import { DownloadManagerModule } from './download-manager/download-manager.module';
1919
import { EventsController } from './infrastructure/controllers/events.controller';
20-
import { AppController } from './infrastructure/controllers/app.controller';
2120
import { AssistantsController } from './infrastructure/controllers/assistants.controller';
2221
import { ChatController } from './infrastructure/controllers/chat.controller';
2322
import { EmbeddingsController } from './infrastructure/controllers/embeddings.controller';
@@ -49,7 +48,7 @@ import { ProcessController } from './infrastructure/controllers/process.controll
4948
DownloadManagerModule,
5049
],
5150
controllers: [
52-
AppController,
51+
EventsController,
5352
AssistantsController,
5453
ChatController,
5554
EmbeddingsController,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export type ModelId = string;
2+
3+
const ModelLoadingEvents = [
4+
'starting',
5+
'stopping',
6+
'started',
7+
'stopped',
8+
'starting-failed',
9+
'stopping-failed',
10+
] as const;
11+
export type ModelLoadingEvent = (typeof ModelLoadingEvents)[number];
12+
13+
const AllModelStates = ['starting', 'stopping', 'started'] as const;
14+
export type ModelState = (typeof AllModelStates)[number];
15+
16+
export interface ModelStatus {
17+
model: ModelId;
18+
status: ModelState;
19+
metadata: Record<string, unknown>;
20+
}
21+
22+
export interface ModelEvent {
23+
model: ModelId;
24+
event: ModelLoadingEvent;
25+
metadata: Record<string, unknown>;
26+
}
27+
28+
export const EmptyModelEvent = {};
29+
30+
export interface ModelStatusAndEvent {
31+
data: {
32+
status: Record<ModelId, ModelStatus>;
33+
event: ModelEvent | typeof EmptyModelEvent;
34+
};
35+
}

cortex-js/src/download-manager/download-manager.service.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { DownloadManagerService } from './download-manager.service';
3+
import { EventEmitterModule } from '@nestjs/event-emitter';
4+
import { HttpModule } from '@nestjs/axios';
35

46
describe('DownloadManagerService', () => {
57
let service: DownloadManagerService;
68

79
beforeEach(async () => {
810
const module: TestingModule = await Test.createTestingModule({
11+
imports: [HttpModule, EventEmitterModule.forRoot()],
912
providers: [DownloadManagerService],
1013
}).compile();
1114

cortex-js/src/infrastructure/controllers/events.controller.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,40 @@ import {
22
DownloadState,
33
DownloadStateEvent,
44
} from '@/domain/models/download.interface';
5+
import {
6+
EmptyModelEvent,
7+
ModelEvent,
8+
ModelId,
9+
ModelStatus,
10+
ModelStatusAndEvent,
11+
} from '@/domain/models/model.event';
512
import { DownloadManagerService } from '@/download-manager/download-manager.service';
13+
import { ModelsUsecases } from '@/usecases/models/models.usecases';
614
import { Controller, Sse } from '@nestjs/common';
715
import { EventEmitter2 } from '@nestjs/event-emitter';
8-
import { Observable, fromEvent, map, merge, of, throttleTime } from 'rxjs';
16+
import { ApiTags } from '@nestjs/swagger';
17+
import {
18+
Observable,
19+
combineLatest,
20+
fromEvent,
21+
map,
22+
merge,
23+
of,
24+
startWith,
25+
throttleTime,
26+
} from 'rxjs';
927

28+
@ApiTags('Events')
1029
@Controller('events')
1130
export class EventsController {
1231
constructor(
1332
private readonly downloadManagerService: DownloadManagerService,
33+
private readonly modelsUsecases: ModelsUsecases,
1434
private readonly eventEmitter: EventEmitter2,
1535
) {}
1636

1737
@Sse('download')
1838
downloadEvent(): Observable<DownloadStateEvent> {
19-
// Welcome message Observable
2039
const latestDownloadState$: Observable<DownloadStateEvent> = of({
2140
data: this.downloadManagerService.getDownloadStates(),
2241
});
@@ -40,4 +59,20 @@ export class EventsController {
4059
downloadAbortEvent$,
4160
).pipe();
4261
}
62+
63+
@Sse('model')
64+
modelEvent(): Observable<ModelStatusAndEvent> {
65+
const latestModelStatus$: Observable<Record<ModelId, ModelStatus>> = of(
66+
this.modelsUsecases.getModelStatuses(),
67+
);
68+
69+
const modelEvent$ = fromEvent<ModelEvent>(
70+
this.eventEmitter,
71+
'model.event',
72+
).pipe(startWith(EmptyModelEvent));
73+
74+
return combineLatest([latestModelStatus$, modelEvent$]).pipe(
75+
map(([status, event]) => ({ data: { status, event } })),
76+
);
77+
}
4378
}

cortex-js/src/usecases/models/models.usecases.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ import { ExtensionModule } from '@/infrastructure/repositories/extensions/extens
66
import { FileManagerModule } from '@/infrastructure/services/file-manager/file-manager.module';
77
import { HttpModule } from '@nestjs/axios';
88
import { ModelRepositoryModule } from '@/infrastructure/repositories/models/model.module';
9+
import { EventEmitterModule } from '@nestjs/event-emitter';
10+
import { DownloadManagerModule } from '@/download-manager/download-manager.module';
911

1012
describe('ModelsService', () => {
1113
let service: ModelsUsecases;
1214

1315
beforeEach(async () => {
1416
const module: TestingModule = await Test.createTestingModule({
1517
imports: [
18+
EventEmitterModule.forRoot(),
1619
DatabaseModule,
1720
ModelsModule,
1821
ExtensionModule,
1922
FileManagerModule,
23+
DownloadManagerModule,
2024
HttpModule,
2125
ModelRepositoryModule,
2226
],

cortex-js/src/usecases/models/models.usecases.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
} from '@/utils/huggingface';
3535
import { DownloadType } from '@/domain/models/download.interface';
3636
import { DownloadManagerService } from '@/download-manager/download-manager.service';
37+
import { EventEmitter2 } from '@nestjs/event-emitter';
38+
import { ModelId, ModelStatus } from '@/domain/models/model.event';
3739

3840
@Injectable()
3941
export class ModelsUsecases {
@@ -43,8 +45,11 @@ export class ModelsUsecases {
4345
private readonly fileManagerService: FileManagerService,
4446
private readonly downloadManagerService: DownloadManagerService,
4547
private readonly httpService: HttpService,
48+
private readonly eventEmitter: EventEmitter2,
4649
) {}
4750

51+
private activeModelStatuses: Record<ModelId, ModelStatus> = {};
52+
4853
/**
4954
* Create a new model
5055
* @param createModelDto Model data
@@ -148,6 +153,17 @@ export class ModelsUsecases {
148153
};
149154
}
150155

156+
// update states and emitting event
157+
this.activeModelStatuses[modelId] = {
158+
model: modelId,
159+
status: 'starting',
160+
metadata: {},
161+
};
162+
this.eventEmitter.emit('model.event', {
163+
id: modelId,
164+
action: 'starting',
165+
});
166+
151167
const parser = new ModelParameterParser();
152168
const loadModelSettings: ModelSettingParams = {
153169
// Default settings
@@ -167,17 +183,40 @@ export class ModelsUsecases {
167183

168184
return engine
169185
.loadModel(model, loadModelSettings)
186+
.then(() => {
187+
this.activeModelStatuses[modelId] = {
188+
model: modelId,
189+
status: 'started',
190+
metadata: {},
191+
};
192+
193+
this.eventEmitter.emit('model.event', {
194+
id: modelId,
195+
action: 'started',
196+
});
197+
})
170198
.then(() => ({
171199
message: 'Model loaded successfully',
172200
modelId,
173201
}))
174-
.catch((e) => ({
175-
message:
176-
e.code === AxiosError.ERR_BAD_REQUEST
177-
? 'Model already loaded'
178-
: 'Model failed to load',
179-
modelId,
180-
}));
202+
.catch((e) => {
203+
// remove the model from this.activeModelStatus.
204+
delete this.activeModelStatuses[modelId];
205+
206+
this.eventEmitter.emit('model.event', {
207+
id: modelId,
208+
action: 'starting-failed',
209+
});
210+
console.error('Starting model failed', e.code, e.message, e.stack);
211+
212+
return {
213+
message:
214+
e.code === AxiosError.ERR_BAD_REQUEST
215+
? 'Model already loaded'
216+
: 'Model failed to load',
217+
modelId,
218+
};
219+
});
181220
}
182221

183222
async stopModel(modelId: string): Promise<StartModelSuccessDto> {
@@ -194,16 +233,41 @@ export class ModelsUsecases {
194233
};
195234
}
196235

236+
this.activeModelStatuses[modelId] = {
237+
model: modelId,
238+
status: 'stopping',
239+
metadata: {},
240+
};
241+
this.eventEmitter.emit('model.event', {
242+
id: modelId,
243+
action: 'stopping',
244+
});
245+
197246
return engine
198247
.unloadModel(modelId)
248+
.then(() => {
249+
delete this.activeModelStatuses[modelId];
250+
251+
this.eventEmitter.emit('model.event', {
252+
id: modelId,
253+
action: 'stopped',
254+
});
255+
})
199256
.then(() => ({
200257
message: 'Model is stopped',
201258
modelId,
202259
}))
203-
.catch(() => ({
204-
message: 'Failed to stop model',
205-
modelId,
206-
}));
260+
.catch(() => {
261+
this.eventEmitter.emit('model.event', {
262+
id: modelId,
263+
action: 'stopping-failed',
264+
});
265+
266+
return {
267+
message: 'Failed to stop model',
268+
modelId,
269+
};
270+
});
207271
}
208272

209273
/**
@@ -377,4 +441,8 @@ export class ModelsUsecases {
377441
if (modelId.includes('/')) return fetchHuggingFaceRepoData(modelId);
378442
else return fetchJanRepoData(modelId);
379443
}
444+
445+
getModelStatuses(): Record<ModelId, ModelStatus> {
446+
return this.activeModelStatuses;
447+
}
380448
}

0 commit comments

Comments
 (0)