import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai'; import { api } from './api'; import { DEFAULT_MODEL_TOKEN_LIMIT, getConfig } from './commands/config'; import { getMainCommitPrompt } from './prompts'; import { mergeDiffs } from './utils/mergeDiffs'; import { tokenCount } from './utils/tokenCount'; const config = getConfig(); const generateCommitMessageChatCompletionPrompt = async ( diff: string ): Promise> => { const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(); const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT]; chatContextAsCompletionRequest.push({ role: ChatCompletionRequestMessageRoleEnum.User, content: diff }); return chatContextAsCompletionRequest; }; export enum GenerateCommitMessageErrorEnum { tooMuchTokens = 'TOO_MUCH_TOKENS', internalError = 'INTERNAL_ERROR', emptyMessage = 'EMPTY_MESSAGE' } const ADJUSTMENT_FACTOR = 20; export const generateCommitMessageByDiff = async ( diff: string ): Promise => { try { const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(); const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map( (msg) => tokenCount(msg.content) + 4 ).reduce((a, b) => a + b, 0); const MAX_REQUEST_TOKENS = DEFAULT_MODEL_TOKEN_LIMIT - ADJUSTMENT_FACTOR - INIT_MESSAGES_PROMPT_LENGTH - config?.OCO_OPENAI_MAX_TOKENS; if (tokenCount(diff) >= MAX_REQUEST_TOKENS) { const commitMessagePromises = await getCommitMsgsPromisesFromFileDiffs( diff, MAX_REQUEST_TOKENS ); const commitMessages = []; for (const promise of commitMessagePromises) { commitMessages.push(await promise); await delay(2000); } return commitMessages.join('\n\n'); } const messages = await generateCommitMessageChatCompletionPrompt(diff); const commitMessage = await api.generateCommitMessage(messages); if (!commitMessage) throw new Error(GenerateCommitMessageErrorEnum.emptyMessage); return commitMessage; } catch (error) { throw error; } }; function getMessagesPromisesByChangesInFile( fileDiff: string, separator: string, maxChangeLength: number ) { const hunkHeaderSeparator = '@@ '; const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator); // merge multiple line-diffs into 1 to save tokens const mergedChanges = mergeDiffs( fileDiffByLines.map((line) => hunkHeaderSeparator + line), maxChangeLength ); const lineDiffsWithHeader = []; for (const change of mergedChanges) { const totalChange = fileHeader + change; if (tokenCount(totalChange) > maxChangeLength) { // If the totalChange is too large, split it into smaller pieces const splitChanges = splitDiff(totalChange, maxChangeLength); lineDiffsWithHeader.push(...splitChanges); } else { lineDiffsWithHeader.push(totalChange); } } const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map( async (lineDiff) => { const messages = await generateCommitMessageChatCompletionPrompt( separator + lineDiff ); return api.generateCommitMessage(messages); } ); return commitMsgsFromFileLineDiffs; } function splitDiff(diff: string, maxChangeLength: number) { const lines = diff.split('\n'); const splitDiffs = []; let currentDiff = ''; for (let line of lines) { // If a single line exceeds maxChangeLength, split it into multiple lines while (tokenCount(line) > maxChangeLength) { const subLine = line.substring(0, maxChangeLength); line = line.substring(maxChangeLength); splitDiffs.push(subLine); } // Check the tokenCount of the currentDiff and the line separately if (tokenCount(currentDiff) + tokenCount('\n' + line) > maxChangeLength) { // If adding the next line would exceed the maxChangeLength, start a new diff splitDiffs.push(currentDiff); currentDiff = line; } else { // Otherwise, add the line to the current diff currentDiff += '\n' + line; } } // Add the last diff if (currentDiff) { splitDiffs.push(currentDiff); } return splitDiffs; } export const getCommitMsgsPromisesFromFileDiffs = async ( diff: string, maxDiffLength: number ) => { const separator = 'diff --git '; const diffByFiles = diff.split(separator).slice(1); // merge multiple files-diffs into 1 prompt to save tokens const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength); const commitMessagePromises = []; for (const fileDiff of mergedFilesDiffs) { if (tokenCount(fileDiff) >= maxDiffLength) { // if file-diff is bigger than gpt context — split fileDiff into lineDiff const messagesPromises = getMessagesPromisesByChangesInFile( fileDiff, separator, maxDiffLength ); commitMessagePromises.push(...messagesPromises); } else { const messages = await generateCommitMessageChatCompletionPrompt( separator + fileDiff ); commitMessagePromises.push(api.generateCommitMessage(messages)); } } return commitMessagePromises; }; function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }