Files
aicommits/src/utils/openai.ts
2023-04-04 02:57:10 -04:00

146 lines
3.4 KiB
TypeScript

import https from 'https';
import type { ClientRequest, IncomingMessage } from 'http';
import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
import { type TiktokenModel } from '@dqbd/tiktoken';
import createHttpsProxyAgent from 'https-proxy-agent';
import { KnownError } from './error.js';
const httpsPost = async (
hostname: string,
path: string,
headers: Record<string, string>,
json: unknown,
proxy?: string,
) => new Promise<{
request: ClientRequest;
response: IncomingMessage;
data: string;
}>((resolve, reject) => {
const postContent = JSON.stringify(json);
const request = https.request(
{
port: 443,
hostname,
path,
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postContent),
},
timeout: 10_000, // 10s
agent: (
proxy
? createHttpsProxyAgent(proxy)
: undefined
),
},
(response) => {
const body: Buffer[] = [];
response.on('data', chunk => body.push(chunk));
response.on('end', () => {
resolve({
request,
response,
data: Buffer.concat(body).toString(),
});
});
},
);
request.on('error', reject);
request.on('timeout', () => {
request.destroy();
reject(new KnownError('Request timed out'));
});
request.write(postContent);
request.end();
});
const createChatCompletion = async (
apiKey: string,
json: CreateChatCompletionRequest,
proxy?: string,
) => {
const { response, data } = await httpsPost(
'api.openai.com',
'/v1/chat/completions',
{
Authorization: `Bearer ${apiKey}`,
},
json,
proxy,
);
if (
!response.statusCode
|| response.statusCode < 200
|| response.statusCode > 299
) {
let errorMessage = `OpenAI API Error: ${response.statusCode} - ${response.statusMessage}`;
if (data) {
errorMessage += `\n\n${data}`;
}
if (response.statusCode === 500) {
errorMessage += '\n\nCheck the API status: https://status.openai.com';
}
throw new KnownError(errorMessage);
}
return JSON.parse(data) as CreateChatCompletionResponse;
};
const sanitizeMessage = (message: string) => message.trim().replace(/[\n\r]/g, '').replace(/(\w)\.$/, '$1');
const deduplicateMessages = (array: string[]) => Array.from(new Set(array));
const getPrompt = (locale: string, diff: string) => `Write an insightful but concise Git commit message in a complete sentence in present tense for the following diff without prefacing it with anything, the response must be in the language ${locale}:\n${diff}`;
export const generateCommitMessage = async (
apiKey: string,
model: TiktokenModel,
locale: string,
diff: string,
completions: number,
proxy?: string,
) => {
const prompt = getPrompt(locale, diff);
try {
const completion = await createChatCompletion(
apiKey,
{
model,
messages: [{
role: 'user',
content: prompt,
}],
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
max_tokens: 200,
stream: false,
n: completions,
},
proxy,
);
return deduplicateMessages(
completion.choices
.filter(choice => choice.message?.content)
.map(choice => sanitizeMessage(choice.message!.content)),
);
} catch (error) {
const errorAsAny = error as any;
if (errorAsAny.code === 'ENOTFOUND') {
throw new KnownError(`Error connecting to ${errorAsAny.hostname} (${errorAsAny.syscall}). Are you connected to the internet?`);
}
throw errorAsAny;
}
};