mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
Feat: Add Claude 3 support (#318)
* 3.0.12 * build * feat: anthropic claude 3 support * fix: add system prompt * fix: type check * fix: package version * fix: update anthropic for dependency bug fix * feat: update build files * feat: update version number --------- Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
This commit is contained in:
17358
out/cli.cjs
17358
out/cli.cjs
File diff suppressed because one or more lines are too long
30699
out/github-action.cjs
30699
out/github-action.cjs
File diff suppressed because one or more lines are too long
Binary file not shown.
3036
package-lock.json
generated
3036
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencommit",
|
||||
"version": "3.0.12",
|
||||
"version": "3.0.13",
|
||||
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
|
||||
"keywords": [
|
||||
"git",
|
||||
@@ -73,6 +73,7 @@
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@anthropic-ai/sdk": "^0.19.2",
|
||||
"@clack/prompts": "^0.6.1",
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@octokit/webhooks-schemas": "^6.11.0",
|
||||
|
||||
@@ -15,6 +15,7 @@ dotenv.config();
|
||||
|
||||
export enum CONFIG_KEYS {
|
||||
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
|
||||
OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY',
|
||||
OCO_TOKENS_MAX_INPUT = 'OCO_TOKENS_MAX_INPUT',
|
||||
OCO_TOKENS_MAX_OUTPUT = 'OCO_TOKENS_MAX_OUTPUT',
|
||||
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
|
||||
@@ -34,6 +35,31 @@ export enum CONFIG_MODES {
|
||||
set = 'set'
|
||||
}
|
||||
|
||||
export const MODEL_LIST = {
|
||||
openai: ['gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0125',
|
||||
'gpt-4',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4-1106-preview',
|
||||
'gpt-4-turbo-preview',
|
||||
'gpt-4-0125-preview'],
|
||||
|
||||
anthropic: ['claude-3-haiku-20240307',
|
||||
'claude-3-sonnet-20240229',
|
||||
'claude-3-opus-20240229']
|
||||
}
|
||||
|
||||
const getDefaultModel = (provider: string | undefined): string => {
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return '';
|
||||
case 'anthropic':
|
||||
return MODEL_LIST.anthropic[0];
|
||||
default:
|
||||
return MODEL_LIST.openai[0];
|
||||
}
|
||||
};
|
||||
|
||||
export enum DEFAULT_TOKEN_LIMITS {
|
||||
DEFAULT_MAX_TOKENS_INPUT = 4096,
|
||||
DEFAULT_MAX_TOKENS_OUTPUT = 500
|
||||
@@ -57,9 +83,9 @@ export const configValidators = {
|
||||
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any, config: any = {}) {
|
||||
//need api key unless running locally with ollama
|
||||
validateConfig(
|
||||
'API_KEY',
|
||||
value || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
||||
'You need to provide an API key'
|
||||
'OpenAI API_KEY',
|
||||
value || config.OCO_ANTHROPIC_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
||||
'You need to provide an OpenAI/Anthropic API key'
|
||||
);
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_OPENAI_API_KEY,
|
||||
@@ -75,6 +101,16 @@ export const configValidators = {
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_ANTHROPIC_API_KEY](value: any, config: any = {}) {
|
||||
validateConfig(
|
||||
'ANTHROPIC_API_KEY',
|
||||
value || config.OCO_OPENAI_API_KEY || config.OCO_AI_PROVIDER == 'ollama' || config.OCO_AI_PROVIDER == 'test',
|
||||
'You need to provide an OpenAI/Anthropic API key'
|
||||
);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_DESCRIPTION](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_DESCRIPTION,
|
||||
@@ -154,19 +190,12 @@ export const configValidators = {
|
||||
[CONFIG_KEYS.OCO_MODEL](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_MODEL,
|
||||
[
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0125',
|
||||
'gpt-4',
|
||||
'gpt-4-1106-preview',
|
||||
'gpt-4-0125-preview',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4-turbo-preview'
|
||||
].includes(value),
|
||||
`${value} is not supported yet, use 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-0125-preview' or 'gpt-4-turbo-preview'`
|
||||
[...MODEL_LIST.openai, ...MODEL_LIST.anthropic].includes(value),
|
||||
`${value} is not supported yet, use 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview', 'gpt-4-0125-preview', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229' or 'claude-3-haiku-20240307'`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
|
||||
[CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER](value: any) {
|
||||
validateConfig(
|
||||
CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
|
||||
@@ -200,10 +229,11 @@ export const configValidators = {
|
||||
[
|
||||
'',
|
||||
'openai',
|
||||
'anthropic',
|
||||
'ollama',
|
||||
'test'
|
||||
].includes(value),
|
||||
`${value} is not supported yet, use 'ollama' or 'openai' (default)`
|
||||
`${value} is not supported yet, use 'ollama' 'anthropic' or 'openai' (default)`
|
||||
);
|
||||
return value;
|
||||
},
|
||||
@@ -228,6 +258,7 @@ const configPath = pathJoin(homedir(), '.opencommit');
|
||||
export const getConfig = (): ConfigType | null => {
|
||||
const configFromEnv = {
|
||||
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
|
||||
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
|
||||
OCO_TOKENS_MAX_INPUT: process.env.OCO_TOKENS_MAX_INPUT
|
||||
? Number(process.env.OCO_TOKENS_MAX_INPUT)
|
||||
: undefined,
|
||||
@@ -237,7 +268,7 @@ export const getConfig = (): ConfigType | null => {
|
||||
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
|
||||
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
|
||||
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
|
||||
OCO_MODEL: process.env.OCO_MODEL || 'gpt-3.5-turbo',
|
||||
OCO_MODEL: process.env.OCO_MODEL || getDefaultModel(process.env.OCO_AI_PROVIDER),
|
||||
OCO_LANGUAGE: process.env.OCO_LANGUAGE || 'en',
|
||||
OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
|
||||
process.env.OCO_MESSAGE_TEMPLATE_PLACEHOLDER || '$msg',
|
||||
|
||||
124
src/engine/anthropic.ts
Normal file
124
src/engine/anthropic.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import {ChatCompletionRequestMessage} from 'openai'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources';
|
||||
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
|
||||
import {
|
||||
CONFIG_MODES,
|
||||
DEFAULT_TOKEN_LIMITS,
|
||||
getConfig
|
||||
} from '../commands/config';
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
|
||||
let provider = config?.OCO_AI_PROVIDER;
|
||||
let apiKey = config?.OCO_ANTHROPIC_API_KEY;
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
if (
|
||||
provider === 'anthropic' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
'OCO_ANTHROPIC_API_KEY is not set, please run `oco config set OCO_ANTHROPIC_API_KEY=<your token> . If you are using Claude, make sure you add payment details, so API works.`'
|
||||
);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL;
|
||||
if (provider === 'anthropic' &&
|
||||
!MODEL_LIST.anthropic.includes(MODEL) &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL} for Anthropic. Supported models are: ${MODEL_LIST.anthropic.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
class AnthropicAi implements AiEngine {
|
||||
private anthropicAiApiConfiguration = {
|
||||
apiKey: apiKey
|
||||
};
|
||||
private anthropicAI!: Anthropic;
|
||||
|
||||
constructor() {
|
||||
this.anthropicAI = new Anthropic(this.anthropicAiApiConfiguration);
|
||||
}
|
||||
|
||||
public generateCommitMessage = async (
|
||||
messages: Array<ChatCompletionRequestMessage>
|
||||
): Promise<string | undefined> => {
|
||||
|
||||
const systemMessage = messages.find(msg => msg.role === 'system')?.content as string;
|
||||
const restMessages = messages.filter((msg) => msg.role !== 'system') as MessageParam[];
|
||||
|
||||
const params: MessageCreateParamsNonStreaming = {
|
||||
model: MODEL,
|
||||
system: systemMessage,
|
||||
messages: restMessages,
|
||||
temperature: 0,
|
||||
top_p: 0.1,
|
||||
max_tokens: MAX_TOKENS_OUTPUT
|
||||
};
|
||||
try {
|
||||
const REQUEST_TOKENS = messages
|
||||
.map((msg) => tokenCount(msg.content as string) + 4)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
if (REQUEST_TOKENS > MAX_TOKENS_INPUT - MAX_TOKENS_OUTPUT) {
|
||||
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
|
||||
}
|
||||
|
||||
const data = await this.anthropicAI.messages.create(params);
|
||||
|
||||
const message = data?.content[0].text;
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
outro(`${chalk.red('✖')} ${JSON.stringify(params)}`);
|
||||
|
||||
const err = error as Error;
|
||||
outro(`${chalk.red('✖')} ${err?.message || err}`);
|
||||
|
||||
if (
|
||||
axios.isAxiosError<{ error?: { message: string } }>(error) &&
|
||||
error.response?.status === 401
|
||||
) {
|
||||
const anthropicAiError = error.response.data.error;
|
||||
|
||||
if (anthropicAiError?.message) outro(anthropicAiError.message);
|
||||
outro(
|
||||
'For help look into README https://github.com/di-sukharev/opencommit#setup'
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicAi = new AnthropicAi();
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
|
||||
import {
|
||||
ChatCompletionRequestMessage,
|
||||
Configuration as OpenAiApiConfiguration,
|
||||
@@ -17,20 +18,28 @@ import {
|
||||
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
|
||||
import { tokenCount } from '../utils/tokenCount';
|
||||
import { AiEngine } from './Engine';
|
||||
import { MODEL_LIST } from '../commands/config';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const MAX_TOKENS_OUTPUT = config?.OCO_TOKENS_MAX_OUTPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT = config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
const MAX_TOKENS_OUTPUT =
|
||||
config?.OCO_TOKENS_MAX_OUTPUT ||
|
||||
DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_OUTPUT;
|
||||
const MAX_TOKENS_INPUT =
|
||||
config?.OCO_TOKENS_MAX_INPUT || DEFAULT_TOKEN_LIMITS.DEFAULT_MAX_TOKENS_INPUT;
|
||||
let basePath = config?.OCO_OPENAI_BASE_PATH;
|
||||
let apiKey = config?.OCO_OPENAI_API_KEY
|
||||
let apiKey = config?.OCO_OPENAI_API_KEY;
|
||||
|
||||
const [command, mode] = process.argv.slice(2);
|
||||
|
||||
const isLocalModel = config?.OCO_AI_PROVIDER == 'ollama' || config?.OCO_AI_PROVIDER == 'test';
|
||||
const provider = config?.OCO_AI_PROVIDER;
|
||||
|
||||
|
||||
if (!apiKey && command !== 'config' && mode !== CONFIG_MODES.set && !isLocalModel) {
|
||||
if (
|
||||
provider === 'openai' &&
|
||||
!apiKey &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set
|
||||
) {
|
||||
intro('opencommit');
|
||||
|
||||
outro(
|
||||
@@ -44,6 +53,18 @@ if (!apiKey && command !== 'config' && mode !== CONFIG_MODES.set && !isLocalMode
|
||||
}
|
||||
|
||||
const MODEL = config?.OCO_MODEL || 'gpt-3.5-turbo';
|
||||
if (provider === 'openai' &&
|
||||
!MODEL_LIST.openai.includes(MODEL) &&
|
||||
command !== 'config' &&
|
||||
mode !== CONFIG_MODES.set) {
|
||||
outro(
|
||||
`${chalk.red('✖')} Unsupported model ${MODEL} for OpenAI. Supported models are: ${MODEL_LIST.openai.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
class OpenAi implements AiEngine {
|
||||
private openAiApiConfiguration = new OpenAiApiConfiguration({
|
||||
@@ -105,6 +126,4 @@ class OpenAi implements AiEngine {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const api = new OpenAi();
|
||||
|
||||
@@ -2,12 +2,15 @@ import { AiEngine } from '../engine/Engine';
|
||||
import { api } from '../engine/openAi';
|
||||
import { getConfig } from '../commands/config';
|
||||
import { ollamaAi } from '../engine/ollama';
|
||||
import { anthropicAi } from '../engine/anthropic'
|
||||
import { testAi } from '../engine/testAi';
|
||||
|
||||
export function getEngine(): AiEngine {
|
||||
const config = getConfig();
|
||||
if (config?.OCO_AI_PROVIDER == 'ollama') {
|
||||
return ollamaAi;
|
||||
} else if (config?.OCO_AI_PROVIDER == 'anthropic') {
|
||||
return anthropicAi;
|
||||
} else if (config?.OCO_AI_PROVIDER == 'test') {
|
||||
return testAi;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user