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, 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; } };