diff --git a/extensions/inference-anthropic-extension/jest.config.js b/extensions/inference-anthropic-extension/jest.config.js new file mode 100644 index 000000000..3e32adceb --- /dev/null +++ b/extensions/inference-anthropic-extension/jest.config.js @@ -0,0 +1,9 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + 'node_modules/@janhq/core/.+\\.(j|t)s?$': 'ts-jest', + }, + transformIgnorePatterns: ['node_modules/(?!@janhq/core/.*)'], +} diff --git a/extensions/inference-anthropic-extension/package.json b/extensions/inference-anthropic-extension/package.json index a9d30a8e5..19c0df5e8 100644 --- a/extensions/inference-anthropic-extension/package.json +++ b/extensions/inference-anthropic-extension/package.json @@ -9,6 +9,7 @@ "author": "Jan ", "license": "AGPL-3.0", "scripts": { + "test": "jest test", "build": "tsc -b . && webpack --config webpack.config.js", "build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install", "sync:core": "cd ../.. && yarn build:core && cd extensions && rm yarn.lock && cd inference-anthropic-extension && yarn && yarn build:publish" diff --git a/extensions/inference-anthropic-extension/src/anthropic.test.ts b/extensions/inference-anthropic-extension/src/anthropic.test.ts new file mode 100644 index 000000000..703ead0fb --- /dev/null +++ b/extensions/inference-anthropic-extension/src/anthropic.test.ts @@ -0,0 +1,77 @@ +// Import necessary modules +import JanInferenceAnthropicExtension, { Settings } from '.' +import { PayloadType, ChatCompletionRole } from '@janhq/core' + +// Mocks +jest.mock('@janhq/core', () => ({ + RemoteOAIEngine: jest.fn().mockImplementation(() => ({ + registerSettings: jest.fn(), + registerModels: jest.fn(), + getSetting: jest.fn(), + onChange: jest.fn(), + onSettingUpdate: jest.fn(), + onLoad: jest.fn(), + headers: jest.fn(), + })), + PayloadType: jest.fn(), + ChatCompletionRole: { + User: 'user' as const, + Assistant: 'assistant' as const, + System: 'system' as const, + }, +})) + +// Helper functions +const createMockPayload = (): PayloadType => ({ + messages: [ + { role: ChatCompletionRole.System, content: 'Meow' }, + { role: ChatCompletionRole.User, content: 'Hello' }, + { role: ChatCompletionRole.Assistant, content: 'Hi there' }, + ], + model: 'claude-v1', + stream: false, +}) + +describe('JanInferenceAnthropicExtension', () => { + let extension: JanInferenceAnthropicExtension + + beforeEach(() => { + extension = new JanInferenceAnthropicExtension('', '') + extension.apiKey = 'mock-api-key' + extension.inferenceUrl = 'mock-endpoint' + jest.clearAllMocks() + }) + + it('should initialize with correct settings', async () => { + await extension.onLoad() + expect(extension.apiKey).toBe('mock-api-key') + expect(extension.inferenceUrl).toBe('mock-endpoint') + }) + + it('should transform payload correctly', () => { + const payload = createMockPayload() + const transformedPayload = extension.transformPayload(payload) + + expect(transformedPayload).toEqual({ + max_tokens: 4096, + model: 'claude-v1', + stream: false, + system: 'Meow', + messages: [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there' }, + ], + }) + }) + + it('should transform response correctly', () => { + const nonStreamResponse = { content: [{ text: 'Test response' }] } + const streamResponse = + 'data: {"type":"content_block_delta","delta":{"text":"Hello"}}' + + expect(extension.transformResponse(nonStreamResponse)).toBe('Test response') + expect(extension.transformResponse(streamResponse)).toBe('Hello') + expect(extension.transformResponse('')).toBe('') + expect(extension.transformResponse('event: something')).toBe('') + }) +}) diff --git a/extensions/inference-anthropic-extension/src/index.ts b/extensions/inference-anthropic-extension/src/index.ts index f28a584f2..94da26d94 100644 --- a/extensions/inference-anthropic-extension/src/index.ts +++ b/extensions/inference-anthropic-extension/src/index.ts @@ -13,7 +13,7 @@ import { ChatCompletionRole } from '@janhq/core' declare const SETTINGS: Array declare const MODELS: Array -enum Settings { +export enum Settings { apiKey = 'anthropic-api-key', chatCompletionsEndPoint = 'chat-completions-endpoint', } @@ -23,6 +23,7 @@ type AnthropicPayloadType = { model?: string max_tokens?: number messages?: Array<{ role: string; content: string }> + system?: string } /** @@ -113,6 +114,10 @@ export default class JanInferenceAnthropicExtension extends RemoteOAIEngine { role: 'assistant', content: item.content as string, }) + } else if (item.role === ChatCompletionRole.System) { + // When using Claude, you can dramatically improve its performance by using the system parameter to give it a role. + // This technique, known as role prompting, is the most powerful way to use system prompts with Claude. + convertedData.system = item.content as string } }) diff --git a/extensions/inference-anthropic-extension/tsconfig.json b/extensions/inference-anthropic-extension/tsconfig.json index 2477d58ce..6db951c9e 100644 --- a/extensions/inference-anthropic-extension/tsconfig.json +++ b/extensions/inference-anthropic-extension/tsconfig.json @@ -10,5 +10,6 @@ "skipLibCheck": true, "rootDir": "./src" }, - "include": ["./src"] + "include": ["./src"], + "exclude": ["**/*.test.ts"] }