Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/azure openai #291

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ dist

# Eslint cache
.eslintcache

# VSCode
.vscode/launch.json
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ aicommits config set OPENAI_KEY=<your-api-key> generate=3 locale=en

#### OPENAI_KEY

Required
It is required unless you opt to use Azure OpenAI.

The OpenAI API key. You can retrieve it from [OpenAI API Keys page](https://platform.openai.com/account/api-keys).

Expand Down Expand Up @@ -239,6 +239,43 @@ You can clear this option by setting it to an empty string:
aicommits config set type=
```

#### USE_AZURE

Default: `false`

The USE_AZURE parameter specifies whether the application will use Azure OpenAI instead of the standard OpenAI API.
It accepts true or false as values.

```sh
aicommits config set USE_AZURE=true
```

You can toggle between OpenAI and Azure OpenAI using that flag.

```sh
aicommits config set USE_AZURE=false
```

#### AZURE_OPENAI_KEY

Required if USE_AZURE=`true`

The Azure OpenAI authentication key.

```sh
aicommits config set AZURE_OPENAI_KEY=<your token>
```

#### AZURE_OPENAI_ENDPOINT

Required if USE_AZURE=`true`

The endpoint URL for your Azure OpenAI deployment. Set it within quotation marks.

```sh
aicommits config set AZURE_OPENAI_ENDPOINT='<your-deployment-full-url>'
```

## How it works

This CLI tool runs `git diff` to grab all your latest code changes, sends them to OpenAI's GPT-3, then returns the AI generated commit message.
Expand Down
12 changes: 1 addition & 11 deletions src/commands/aicommits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,7 @@ export default async (
s.start('The AI is analyzing your changes');
let messages: string[];
try {
messages = await generateCommitMessage(
config.OPENAI_KEY,
config.model,
config.locale,
staged.diff,
config.generate,
config['max-length'],
config.type,
config.timeout,
config.proxy
);
messages = await generateCommitMessage(config, staged.diff);
} finally {
s.stop('Changes analyzed');
}
Expand Down
13 changes: 12 additions & 1 deletion src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,18 @@ export default command(

if (mode === 'set') {
await setConfigs(
keyValues.map((keyValue) => keyValue.split('=') as [string, string])
keyValues.map((keyValue) => {
const [key, ...valueParts] = keyValue.split('=');
let value = valueParts.join('=');

if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
} else if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}

return [key, value] as [string, string];
})
);
return;
}
Expand Down
12 changes: 1 addition & 11 deletions src/commands/prepare-commit-msg-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,7 @@ export default () =>
s.start('The AI is analyzing your changes');
let messages: string[];
try {
messages = await generateCommitMessage(
config.OPENAI_KEY,
config.model,
config.locale,
staged!.diff,
config.generate,
config['max-length'],
config.type,
config.timeout,
config.proxy
);
messages = await generateCommitMessage(config, staged!.diff);
} finally {
s.stop('Changes analyzed');
}
Expand Down
87 changes: 76 additions & 11 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'fs/promises';
import path from 'path';
import path, { parse } from 'path';
import os from 'os';
import ini from 'ini';
import type { TiktokenModel } from '@dqbd/tiktoken';
Expand All @@ -23,9 +23,8 @@ const parseAssert = (name: string, condition: any, message: string) => {
const configParsers = {
OPENAI_KEY(key?: string) {
if (!key) {
throw new KnownError(
'Please set your OpenAI API key via `aicommits config set OPENAI_KEY=<your token>`'
);
// if USE_AZURE config is set to true, then we don't need to check for OPENAI_KEY
return '';
}
parseAssert('OPENAI_KEY', key.startsWith('sk-'), 'Must start with "sk-"');
// Key can range from 43~51 characters. There's no spec to assert this.
Expand Down Expand Up @@ -58,7 +57,7 @@ const configParsers = {

return parsed;
},
type(type?: string) {
type(type?: string): CommitType {
if (!type) {
return '';
}
Expand Down Expand Up @@ -117,14 +116,52 @@ const configParsers = {
},
} as const;

type ConfigKeys = keyof typeof configParsers;
const azureConfigParsers = {
USE_AZURE(useAzure?: string) {
if (useAzure === undefined) {
return false;
}
const normalizedValue = String(useAzure).toLowerCase();
parseAssert(
'USE_AZURE',
normalizedValue === 'true' || normalizedValue === 'false',
'Must be true or false'
);
return normalizedValue === 'true';
},
AZURE_OPENAI_KEY(key?: string) {
if (!key) {
return '';
}
parseAssert('AZURE_OPENAI_KEY', key.length > 0, 'Cannot be empty');
return key;
},
AZURE_OPENAI_ENDPOINT(endpoint?: string) {
if (!endpoint) {
return '';
}
parseAssert(
'AZURE_OPENAI_ENDPOINT',
/^https?:\/\//.test(endpoint),
'Must be a valid URL'
);
return endpoint;
},
} as const;

const combinedParsers = {
...azureConfigParsers,
...configParsers,
};

type ConfigKeys = keyof typeof combinedParsers;

type RawConfig = {
[key in ConfigKeys]?: string;
};

export type ValidConfig = {
[Key in ConfigKeys]: ReturnType<(typeof configParsers)[Key]>;
[Key in ConfigKeys]: ReturnType<(typeof combinedParsers)[Key]>;
};

const configPath = path.join(os.homedir(), '.aicommits');
Expand All @@ -146,8 +183,8 @@ export const getConfig = async (
const config = await readConfigFile();
const parsedConfig: Record<string, unknown> = {};

for (const key of Object.keys(configParsers) as ConfigKeys[]) {
const parser = configParsers[key];
for (const key of Object.keys(combinedParsers) as ConfigKeys[]) {
const parser = combinedParsers[key];
const value = cliConfig?.[key] ?? config[key];

if (suppressErrors) {
Expand All @@ -159,18 +196,46 @@ export const getConfig = async (
}
}

const openaiKey = parsedConfig['OPENAI_KEY'] as string;
const useAzure = parsedConfig['USE_AZURE'] as boolean;
if (openaiKey === '' && !useAzure) {
if (!suppressErrors) {
throw new KnownError(
'Please set your OpenAI API key via `aicommits config set OPENAI_KEY=<your token>` or set your Azure OpenAI configurations.'
);
}
}

const azureOpenaiKey = parsedConfig['AZURE_OPENAI_KEY'] as string;
const azureOpenaiEndpoint = parsedConfig['AZURE_OPENAI_ENDPOINT'] as string;
if (useAzure) {
if (!suppressErrors) {
if (azureOpenaiKey === '') {
throw new KnownError(
`Please set your Azure OpenAI configurations via aicommits config set AZURE_OPENAI_KEY=<your token>`
);
}

if (azureOpenaiEndpoint === '') {
throw new KnownError(
`Please set your Azure OpenAI configurations via aicommits config set AZURE_OPENAI_ENDPOINT='<your-deployment-full-url>'`
);
}
}
}

return parsedConfig as ValidConfig;
};

export const setConfigs = async (keyValues: [key: string, value: string][]) => {
const config = await readConfigFile();

for (const [key, value] of keyValues) {
if (!hasOwn(configParsers, key)) {
if (!hasOwn(combinedParsers, key)) {
throw new KnownError(`Invalid config property: ${key}`);
}

const parsed = configParsers[key as ConfigKeys](value);
const parsed = combinedParsers[key as ConfigKeys](value);
config[key as ConfigKeys] = parsed as any;
}

Expand Down
76 changes: 34 additions & 42 deletions src/utils/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,13 @@ import type {
CreateChatCompletionRequest,
CreateChatCompletionResponse,
} from 'openai';
import {
type TiktokenModel,
// encoding_for_model,
} from '@dqbd/tiktoken';
import createHttpsProxyAgent from 'https-proxy-agent';
import { KnownError } from './error.js';
import type { CommitType } from './config.js';
import type { ValidConfig } from './config.js';
import { generatePrompt } from './prompt.js';

const httpsPost = async (
hostname: string,
path: string,
url: string,
headers: Record<string, string>,
json: unknown,
timeout: number,
Expand All @@ -28,10 +23,9 @@ const httpsPost = async (
}>((resolve, reject) => {
const postContent = JSON.stringify(json);
const request = https.request(
url,
{
port: 443,
hostname,
path,
method: 'POST',
headers: {
...headers,
Expand Down Expand Up @@ -68,17 +62,20 @@ const httpsPost = async (
});

const createChatCompletion = async (
useAzure: boolean,
url: string,
apiKey: string,
json: CreateChatCompletionRequest,
timeout: number,
proxy?: string
) => {
const headers: Record<string, string> = useAzure
? { 'api-key': apiKey }
: { Authorization: `Bearer ${apiKey}` };

const { response, data } = await httpsPost(
'api.openai.com',
'/v1/chat/completions',
{
Authorization: `Bearer ${apiKey}`,
},
url,
headers,
json,
timeout,
proxy
Expand All @@ -89,7 +86,7 @@ const createChatCompletion = async (
response.statusCode < 200 ||
response.statusCode > 299
) {
let errorMessage = `OpenAI API Error: ${response.statusCode} - ${response.statusMessage}`;
let errorMessage = `API Error: ${response.statusCode} - ${response.statusMessage}`;

if (data) {
errorMessage += `\n\n${data}`;
Expand All @@ -113,37 +110,32 @@ const sanitizeMessage = (message: string) =>

const deduplicateMessages = (array: string[]) => Array.from(new Set(array));

// const generateStringFromLength = (length: number) => {
// let result = '';
// const highestTokenChar = 'z';
// for (let i = 0; i < length; i += 1) {
// result += highestTokenChar;
// }
// return result;
// };

// const getTokens = (prompt: string, model: TiktokenModel) => {
// const encoder = encoding_for_model(model);
// const tokens = encoder.encode(prompt).length;
// // Free the encoder to avoid possible memory leaks.
// encoder.free();
// return tokens;
// };

export const generateCommitMessage = async (
apiKey: string,
model: TiktokenModel,
locale: string,
diff: string,
completions: number,
maxLength: number,
type: CommitType,
timeout: number,
proxy?: string
config: ValidConfig,
diff: string
) => {
const {
OPENAI_KEY: apiKey,
USE_AZURE: useAzure,
AZURE_OPENAI_KEY: azureKey,
proxy,
generate: completions,
timeout,
locale,
'max-length': maxLength,
model,
type,
} = config;

try {
const url = useAzure
? config.AZURE_OPENAI_ENDPOINT
: 'https://api.openai.com/v1/chat/completions';

const completion = await createChatCompletion(
apiKey,
useAzure,
url,
useAzure ? azureKey! : apiKey!,
{
model,
messages: [
Expand Down
Loading