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

Commit 0511475

Browse files
feat: support openrouter, cohere engine (#946)
1 parent 779907f commit 0511475

File tree

8 files changed

+184
-2
lines changed

8 files changed

+184
-2
lines changed

cortex-js/src/domain/abstracts/oai.abstract.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export abstract class OAIEngineExtension extends EngineExtension {
4040
}),
4141
);
4242

43+
4344
if (!response) {
4445
throw new Error('No response');
4546
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import stream from 'stream';
2+
import { HttpService } from '@nestjs/axios';
3+
import { OAIEngineExtension } from '../domain/abstracts/oai.abstract';
4+
import { ConfigsUsecases } from '@/usecases/configs/configs.usecase';
5+
import { EventEmitter2 } from '@nestjs/event-emitter';
6+
import _ from 'lodash';
7+
import { EngineStatus } from '@/domain/abstracts/engine.abstract';
8+
import { ChatCompletionMessage } from '@/infrastructure/dtos/chat/chat-completion-message.dto';
9+
10+
enum RoleType {
11+
user = 'USER',
12+
chatbot = 'CHATBOT',
13+
system = 'SYSTEM',
14+
}
15+
16+
type CoherePayloadType = {
17+
chat_history?: Array<{ role: RoleType; message: string }>
18+
message?: string
19+
preamble?: string
20+
}
21+
22+
/**
23+
* A class that implements the InferenceExtension interface from the @janhq/core package.
24+
* The class provides methods for initializing and stopping a model, and for making inference requests.
25+
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
26+
*/
27+
export default class CoHereEngineExtension extends OAIEngineExtension {
28+
apiUrl = 'https://api.cohere.ai/v1/chat';
29+
name = 'cohere';
30+
productName = 'Cohere Inference Engine';
31+
description = 'This extension enables Cohere chat completion API calls';
32+
version = '0.0.1';
33+
apiKey?: string;
34+
35+
constructor(
36+
protected readonly httpService: HttpService,
37+
protected readonly configsUsecases: ConfigsUsecases,
38+
protected readonly eventEmmitter: EventEmitter2,
39+
) {
40+
super(httpService);
41+
42+
eventEmmitter.on('config.updated', async (data) => {
43+
if (data.engine === this.name) {
44+
this.apiKey = data.value;
45+
this.status =
46+
(this.apiKey?.length ?? 0) > 0
47+
? EngineStatus.READY
48+
: EngineStatus.MISSING_CONFIGURATION;
49+
}
50+
});
51+
}
52+
53+
async onLoad() {
54+
const configs = (await this.configsUsecases.getGroupConfigs(
55+
this.name,
56+
)) as unknown as { apiKey: string };
57+
this.apiKey = configs?.apiKey;
58+
this.status =
59+
(this.apiKey?.length ?? 0) > 0
60+
? EngineStatus.READY
61+
: EngineStatus.MISSING_CONFIGURATION;
62+
}
63+
64+
transformPayload = (payload: any): CoherePayloadType => {
65+
console.log('payload', payload)
66+
if (payload.messages.length === 0) {
67+
return {}
68+
}
69+
70+
const { messages, ...params } = payload;
71+
const convertedData: CoherePayloadType = {
72+
...params,
73+
chat_history: [],
74+
message: '',
75+
};
76+
(messages as ChatCompletionMessage[]).forEach((item: ChatCompletionMessage, index: number) => {
77+
// Assign the message of the last item to the `message` property
78+
if (index === messages.length - 1) {
79+
convertedData.message = item.content as string
80+
return
81+
}
82+
if (item.role === 'user') {
83+
convertedData.chat_history!!.push({
84+
role: 'USER' as RoleType,
85+
message: item.content as string,
86+
})
87+
} else if (item.role === 'assistant') {
88+
convertedData.chat_history!!.push({
89+
role: 'CHATBOT' as RoleType,
90+
message: item.content as string,
91+
})
92+
} else if (item.role === 'system') {
93+
convertedData.preamble = item.content as string
94+
}
95+
})
96+
return convertedData
97+
}
98+
99+
transformResponse = (data: any) => {
100+
const text = typeof data === 'object' ? data.text : JSON.parse(data).text ?? ''
101+
return JSON.stringify({
102+
choices: [
103+
{
104+
delta: {
105+
content: text,
106+
},
107+
},
108+
],
109+
});
110+
}
111+
}

cortex-js/src/extensions/extensions.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ConfigsUsecases } from '@/usecases/configs/configs.usecase';
77
import { ConfigsModule } from '@/usecases/configs/configs.module';
88
import { EventEmitter2 } from '@nestjs/event-emitter';
99
import AnthropicEngineExtension from './anthropic.engine';
10+
import OpenRouterEngineExtension from './openrouter.engine';
11+
import CoHereEngineExtension from './cohere.engine';
1012

1113
const provider = {
1214
provide: 'EXTENSIONS_PROVIDER',
@@ -20,6 +22,8 @@ const provider = {
2022
new GroqEngineExtension(httpService, configUsecases, eventEmitter),
2123
new MistralEngineExtension(httpService, configUsecases, eventEmitter),
2224
new AnthropicEngineExtension(httpService, configUsecases, eventEmitter),
25+
new OpenRouterEngineExtension(httpService, configUsecases, eventEmitter),
26+
new CoHereEngineExtension(httpService, configUsecases, eventEmitter),
2327
],
2428
};
2529

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import stream from 'stream';
2+
import { HttpService } from '@nestjs/axios';
3+
import { OAIEngineExtension } from '../domain/abstracts/oai.abstract';
4+
import { ConfigsUsecases } from '@/usecases/configs/configs.usecase';
5+
import { EventEmitter2 } from '@nestjs/event-emitter';
6+
import _ from 'lodash';
7+
import { EngineStatus } from '@/domain/abstracts/engine.abstract';
8+
9+
/**
10+
* A class that implements the InferenceExtension interface from the @janhq/core package.
11+
* The class provides methods for initializing and stopping a model, and for making inference requests.
12+
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
13+
*/
14+
export default class OpenRouterEngineExtension extends OAIEngineExtension {
15+
apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
16+
name = 'openrouter';
17+
productName = 'OpenRouter Inference Engine';
18+
description = 'This extension enables OpenRouter chat completion API calls';
19+
version = '0.0.1';
20+
apiKey?: string;
21+
22+
constructor(
23+
protected readonly httpService: HttpService,
24+
protected readonly configsUsecases: ConfigsUsecases,
25+
protected readonly eventEmmitter: EventEmitter2,
26+
) {
27+
super(httpService);
28+
29+
eventEmmitter.on('config.updated', async (data) => {
30+
if (data.engine === this.name) {
31+
this.apiKey = data.value;
32+
this.status =
33+
(this.apiKey?.length ?? 0) > 0
34+
? EngineStatus.READY
35+
: EngineStatus.MISSING_CONFIGURATION;
36+
}
37+
});
38+
}
39+
40+
async onLoad() {
41+
const configs = (await this.configsUsecases.getGroupConfigs(
42+
this.name,
43+
)) as unknown as { apiKey: string };
44+
this.apiKey = configs?.apiKey;
45+
this.status =
46+
(this.apiKey?.length ?? 0) > 0
47+
? EngineStatus.READY
48+
: EngineStatus.MISSING_CONFIGURATION;
49+
}
50+
51+
transformPayload = (data: any): any => {
52+
return {
53+
...data,
54+
model:"openrouter/auto",
55+
}
56+
};
57+
58+
}

cortex-js/src/infrastructure/commanders/models/model-pull.command.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { downloadProgress } from '@/utils/download-progress';
1919
import { CortexClient } from '../services/cortex.client';
2020
import { DownloadType } from '@/domain/models/download.interface';
2121
import ora from 'ora';
22-
import { isLocalFile } from '@/utils/urls';
22+
import { isRemoteEngine } from '@/utils/normalize-model-id';
2323

2424
@SubCommand({
2525
name: 'pull',
@@ -70,6 +70,7 @@ export class ModelPullCommand extends BaseCommand {
7070

7171
// Pull engine if not exist
7272
if (
73+
!isRemoteEngine(engine) &&
7374
!existsSync(join(await this.fileService.getCortexCppEnginePath(), engine))
7475
) {
7576
console.log('\n');

cortex-js/src/infrastructure/commanders/types/engine.interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export enum Engines {
88
mistral = 'mistral',
99
openai = 'openai',
1010
anthropic = 'anthropic',
11+
openrouter = 'openrouter',
12+
cohere = 'cohere',
1113
}
1214

1315
export const EngineNamesMap: {
@@ -23,4 +25,6 @@ export const RemoteEngines: Engines[] = [
2325
Engines.mistral,
2426
Engines.openai,
2527
Engines.anthropic,
28+
Engines.openrouter,
29+
Engines.cohere,
2630
];

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ export class EnginesController {
111111
description: 'The unique identifier of the engine.',
112112
})
113113
@Patch(':name(*)')
114-
update(@Param('name') name: string, @Body() configs: ConfigUpdateDto) {
114+
update(@Param('name') name: string, @Body() configs?: any | undefined) {
115+
console.log('configs', configs)
115116
return this.enginesUsecases.updateConfigs(
116117
configs.config,
117118
configs.value,

cortex-js/src/infrastructure/services/download-manager/download-manager.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export class DownloadManagerService {
188188

189189
writer.on('finish', () => {
190190
try {
191+
if (timeoutId) clearTimeout(timeoutId);
191192
// delete the abort controller
192193
delete this.abortControllers[downloadId][destination];
193194
const currentDownloadState = this.allDownloadStates.find(
@@ -210,6 +211,7 @@ export class DownloadManagerService {
210211
});
211212
writer.on('error', (error) => {
212213
try {
214+
if (timeoutId) clearTimeout(timeoutId);
213215
this.handleError(error, downloadId, destination);
214216
} finally {
215217
bar.stop();

0 commit comments

Comments
 (0)