146 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
};
|