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

Commit 05d181c

Browse files
feat: support anthropic engine
1 parent f13dfa6 commit 05d181c

File tree

7 files changed

+137
-10
lines changed

7 files changed

+137
-10
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { Extension } from './extension.abstract';
66
export abstract class EngineExtension extends Extension {
77
abstract onLoad(): void;
88

9+
transformPayload?: Function;
10+
11+
transformResponse?: Function;
12+
913
abstract inference(
1014
dto: any,
1115
headers: Record<string, string>,
@@ -17,4 +21,5 @@ export abstract class EngineExtension extends Extension {
1721
): Promise<void> {}
1822

1923
async unloadModel(modelId: string): Promise<void> {}
24+
2025
}

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { HttpService } from '@nestjs/axios';
22
import { EngineExtension } from './engine.abstract';
3-
import stream from 'stream';
3+
import stream, { Transform } from 'stream';
44
import { firstValueFrom } from 'rxjs';
5+
import _ from 'lodash';
56

67
export abstract class OAIEngineExtension extends EngineExtension {
78
abstract apiUrl: string;
@@ -17,22 +18,47 @@ export abstract class OAIEngineExtension extends EngineExtension {
1718
createChatDto: any,
1819
headers: Record<string, string>,
1920
): Promise<stream.Readable | any> {
20-
const { stream } = createChatDto;
21+
const payload = this.transformPayload ? this.transformPayload(createChatDto) : createChatDto;
22+
const { stream: isStream } = payload;
23+
const additionalHeaders = _.omit(headers, ['content-type', 'authorization']);
2124
const response = await firstValueFrom(
22-
this.httpService.post(this.apiUrl, createChatDto, {
25+
this.httpService.post(this.apiUrl, payload, {
2326
headers: {
2427
'Content-Type': headers['content-type'] ?? 'application/json',
2528
Authorization: this.apiKey
2629
? `Bearer ${this.apiKey}`
2730
: headers['authorization'],
31+
...additionalHeaders,
2832
},
29-
responseType: stream ? 'stream' : 'json',
33+
responseType: isStream ? 'stream' : 'json',
3034
}),
3135
);
36+
3237
if (!response) {
3338
throw new Error('No response');
3439
}
35-
36-
return response.data;
40+
if(!this.transformResponse) {
41+
return response.data;
42+
}
43+
if (isStream) {
44+
const transformResponse = this.transformResponse.bind(this);
45+
const lineStream = new Transform({
46+
transform(chunk, encoding, callback) {
47+
const lines = chunk.toString().split('\n');
48+
const transformedLines = [];
49+
for (const line of lines) {
50+
if (line.trim().length > 0) {
51+
const transformedLine = transformResponse(line);
52+
if (transformedLine) {
53+
transformedLines.push(transformedLine);
54+
}
55+
}
56+
}
57+
callback(null, transformedLines.join(''));
58+
}
59+
});
60+
return response.data.pipe(lineStream);
61+
}
62+
return this.transformResponse(response.data);
3763
}
3864
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
8+
/**
9+
* A class that implements the InferenceExtension interface from the @janhq/core package.
10+
* The class provides methods for initializing and stopping a model, and for making inference requests.
11+
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
12+
*/
13+
export default class AnthropicEngineExtension extends OAIEngineExtension {
14+
apiUrl = 'https://api.anthropic.com/v1/messages';
15+
name = 'anthropic';
16+
productName = 'Anthropic Inference Engine';
17+
description = 'This extension enables Anthropic chat completion API calls';
18+
version = '0.0.1';
19+
apiKey?: string;
20+
21+
constructor(
22+
protected readonly httpService: HttpService,
23+
protected readonly configsUsecases: ConfigsUsecases,
24+
protected readonly eventEmmitter: EventEmitter2,
25+
) {
26+
super(httpService);
27+
28+
eventEmmitter.on('config.updated', async (data) => {
29+
if (data.group === this.name) {
30+
this.apiKey = data.value;
31+
}
32+
});
33+
}
34+
35+
async onLoad() {
36+
const configs = (await this.configsUsecases.getGroupConfigs(
37+
this.name,
38+
)) as unknown as { apiKey: string };
39+
this.apiKey = configs?.apiKey;
40+
if (!configs?.apiKey)
41+
await this.configsUsecases.saveConfig('apiKey', '', this.name);
42+
}
43+
44+
override async inference(dto: any, headers: Record<string, string>): Promise<stream.Readable | any> {
45+
headers['x-api-key'] = this.apiKey as string
46+
headers['Content-Type'] = 'application/json'
47+
headers['anthropic-version'] = '2023-06-01'
48+
return super.inference(dto, headers)
49+
}
50+
51+
transformPayload = (data: any): any => {
52+
return _.pick(data, ['messages', 'model', 'stream', 'max_tokens']);
53+
}
54+
55+
transformResponse = (data: any): string => {
56+
// handling stream response
57+
if (typeof data === 'string' && data.trim().length === 0) {
58+
return '';
59+
}
60+
if (typeof data === 'string' && data.startsWith('event: ')) {
61+
return ''
62+
}
63+
if (typeof data === 'string' && data.startsWith('data: ')) {
64+
data = data.replace('data: ', '');
65+
const parsedData = JSON.parse(data);
66+
if (parsedData.type !== 'content_block_delta') {
67+
return ''
68+
}
69+
const text = parsedData.delta?.text;
70+
//convert to have this format data.choices[0]?.delta?.content
71+
return JSON.stringify({
72+
choices: [
73+
{
74+
delta: {
75+
content: text
76+
}
77+
}
78+
]
79+
})
80+
}
81+
// non-stream response
82+
if (data.content && data.content.length > 0 && data.content[0].text) {
83+
return JSON.stringify({
84+
choices: [
85+
{
86+
delta: {
87+
content: data.content[0].text,
88+
},
89+
},
90+
],
91+
});
92+
}
93+
94+
console.error('Invalid response format:', data);
95+
return '';
96+
}
97+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { HttpModule, HttpService } from '@nestjs/axios';
66
import { ConfigsUsecases } from '@/usecases/configs/configs.usecase';
77
import { ConfigsModule } from '@/usecases/configs/configs.module';
88
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
9+
import AnthropicEngineExtension from './anthropic.engine';
910

1011
const provider = {
1112
provide: 'EXTENSIONS_PROVIDER',
@@ -18,6 +19,7 @@ const provider = {
1819
new OpenAIEngineExtension(httpService, configUsecases, eventEmitter),
1920
new GroqEngineExtension(httpService, configUsecases, eventEmitter),
2021
new MistralEngineExtension(httpService, configUsecases, eventEmitter),
22+
new AnthropicEngineExtension(httpService, configUsecases, eventEmitter),
2123
],
2224
};
2325

cortex-js/src/infrastructure/commanders/chat.command.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ export class ChatCommand extends CommandRunner {
4949

5050
async run(passedParams: string[], options: ChatOptions): Promise<void> {
5151
let modelId = passedParams[0];
52-
const checkingSpinner = ora('Checking model...').start();
5352
// First attempt to get message from input or options
5453
// Extract input from 1 to end of array
5554
let message = options.message ?? passedParams.slice(1).join(' ');
@@ -68,11 +67,9 @@ export class ChatCommand extends CommandRunner {
6867
} else if (models.length > 0) {
6968
modelId = await this.modelInquiry(models);
7069
} else {
71-
checkingSpinner.fail('Model ID is required');
7270
exit(1);
7371
}
7472
}
75-
checkingSpinner.succeed(`Model found`);
7673

7774
if (!message) options.attach = true;
7875
const result = await this.chatCliUsecases.chat(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export enum Engines {
77
groq = 'groq',
88
mistral = 'mistral',
99
openai = 'openai',
10+
anthropic = 'anthropic',
1011
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export class ChatUsecases {
3535
const engine = (await this.extensionRepository.findOne(
3636
model!.engine ?? Engines.llamaCPP,
3737
)) as EngineExtension | undefined;
38-
3938
if (engine == null) {
4039
throw new Error(`No engine found with name: ${model.engine}`);
4140
}

0 commit comments

Comments
 (0)