Skip to content

Commit 32f3e17

Browse files
feat(api.ts): solving bad request issue (#187)
* 2.0.18 * patch * 2.0.19 * style(.prettierrc): reorder properties to follow alphabetical order and improve readability * feat(generateCommitMessageFromGitDiff.ts): changing logic of MAX_REQ_TOKENS fix(api.ts): add missing import for GenerateCommitMessageErrorEnum The token count validation is added to prevent the request from exceeding the default model token limit. The validation is done by counting the tokens in each message and adding 4 to each count to account for the additional tokens added by the API. If the total token count exceeds the limit, an error is thrown. The missing import for GenerateCommitMessageErrorEnum is also added. feat: add support for splitting long line-diffs into smaller pieces This change adds support for splitting long line-diffs into smaller pieces to avoid exceeding the maximum commit message length. The `splitDiff` function splits a single line into multiple lines if it exceeds the maximum length. It also splits the diff into smaller pieces if adding the next line would exceed the maximum length. This change improves the readability of commit messages and makes them more consistent. refactor: improve code readability by adding whitespace and reformatting code This commit improves the readability of the code by adding whitespace and reformatting the code. The changes do not affect the functionality of the code. Additionally, a new function `delay` has been added to the file. --------- Co-authored-by: di-sukharev <dim.sukharev@gmail.com>
1 parent 4f57201 commit 32f3e17

File tree

3 files changed

+171
-104
lines changed

3 files changed

+171
-104
lines changed

src/api.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
OpenAIApi
88
} from 'openai';
99

10-
import { CONFIG_MODES, getConfig } from './commands/config';
10+
import {CONFIG_MODES, DEFAULT_MODEL_TOKEN_LIMIT, getConfig} from './commands/config';
11+
import {tokenCount} from './utils/tokenCount';
12+
import {GenerateCommitMessageErrorEnum} from './generateCommitMessageFromGitDiff';
1113

1214
const config = getConfig();
1315

@@ -56,6 +58,14 @@ class OpenAi {
5658
max_tokens: maxTokens || 500
5759
};
5860
try {
61+
const REQUEST_TOKENS = messages.map(
62+
(msg) => tokenCount(msg.content) + 4
63+
).reduce((a, b) => a + b, 0);
64+
65+
if (REQUEST_TOKENS > (DEFAULT_MODEL_TOKEN_LIMIT - maxTokens)) {
66+
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
67+
}
68+
5969
const { data } = await this.openAI.createChatCompletion(params);
6070

6171
const message = data.choices[0].message;

src/commands/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export enum CONFIG_KEYS {
2222
OCO_LANGUAGE = 'OCO_LANGUAGE'
2323
}
2424

25+
export const DEFAULT_MODEL_TOKEN_LIMIT = 4096;
26+
2527
export enum CONFIG_MODES {
2628
get = 'get',
2729
set = 'set'
Lines changed: 158 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
import {
2-
ChatCompletionRequestMessage,
3-
ChatCompletionRequestMessageRoleEnum
2+
ChatCompletionRequestMessage,
3+
ChatCompletionRequestMessageRoleEnum
44
} from 'openai';
5-
import { api } from './api';
6-
import { getConfig } from './commands/config';
7-
import { mergeDiffs } from './utils/mergeDiffs';
8-
import { i18n, I18nLocals } from './i18n';
9-
import { tokenCount } from './utils/tokenCount';
5+
import {api} from './api';
6+
import {DEFAULT_MODEL_TOKEN_LIMIT, getConfig} from './commands/config';
7+
import {mergeDiffs} from './utils/mergeDiffs';
8+
import {i18n, I18nLocals} from './i18n';
9+
import {tokenCount} from './utils/tokenCount';
1010

1111
const config = getConfig();
1212
const translation = i18n[(config?.OCO_LANGUAGE as I18nLocals) || 'en'];
1313

1414
const INIT_MESSAGES_PROMPT: Array<ChatCompletionRequestMessage> = [
15-
{
16-
role: ChatCompletionRequestMessageRoleEnum.System,
17-
// prettier-ignore
18-
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
19-
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.': 'Do not preface the commit with anything.'}
20-
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.': "Don't add any descriptions to the commit, only commit message."}
15+
{
16+
role: ChatCompletionRequestMessageRoleEnum.System,
17+
// prettier-ignore
18+
content: `You are to act as the author of a commit message in git. Your mission is to create clean and comprehensive commit messages in the conventional commit convention and explain WHAT were the changes and WHY the changes were done. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
19+
${config?.OCO_EMOJI ? 'Use GitMoji convention to preface the commit.' : 'Do not preface the commit with anything.'}
20+
${config?.OCO_DESCRIPTION ? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.' : "Don't add any descriptions to the commit, only commit message."}
2121
Use the present tense. Lines must not be longer than 74 characters. Use ${translation.localLanguage} to answer.`
22-
},
23-
{
24-
role: ChatCompletionRequestMessageRoleEnum.User,
25-
content: `diff --git a/src/server.ts b/src/server.ts
22+
},
23+
{
24+
role: ChatCompletionRequestMessageRoleEnum.User,
25+
content: `diff --git a/src/server.ts b/src/server.ts
2626
index ad4db42..f3b18a9 100644
2727
--- a/src/server.ts
2828
+++ b/src/server.ts
@@ -46,128 +46,183 @@ app.use((_, res, next) => {
4646
+app.listen(process.env.PORT || PORT, () => {
4747
+ console.log(\`Server listening on port \${PORT}\`);
4848
});`
49-
},
50-
{
51-
role: ChatCompletionRequestMessageRoleEnum.Assistant,
52-
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
49+
},
50+
{
51+
role: ChatCompletionRequestMessageRoleEnum.Assistant,
52+
content: `${config?.OCO_EMOJI ? '🐛 ' : ''}${translation.commitFix}
5353
${config?.OCO_EMOJI ? '✨ ' : ''}${translation.commitFeat}
5454
${config?.OCO_DESCRIPTION ? translation.commitDescription : ''}`
55-
}
55+
}
5656
];
5757

5858
const generateCommitMessageChatCompletionPrompt = (
59-
diff: string
59+
diff: string
6060
): Array<ChatCompletionRequestMessage> => {
61-
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
61+
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
6262

63-
chatContextAsCompletionRequest.push({
64-
role: ChatCompletionRequestMessageRoleEnum.User,
65-
content: diff
66-
});
63+
chatContextAsCompletionRequest.push({
64+
role: ChatCompletionRequestMessageRoleEnum.User,
65+
content: diff
66+
});
6767

68-
return chatContextAsCompletionRequest;
68+
return chatContextAsCompletionRequest;
6969
};
7070

7171
export enum GenerateCommitMessageErrorEnum {
72-
tooMuchTokens = 'TOO_MUCH_TOKENS',
73-
internalError = 'INTERNAL_ERROR',
74-
emptyMessage = 'EMPTY_MESSAGE'
72+
tooMuchTokens = 'TOO_MUCH_TOKENS',
73+
internalError = 'INTERNAL_ERROR',
74+
emptyMessage = 'EMPTY_MESSAGE'
7575
}
7676

77-
7877
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
79-
(msg) => tokenCount(msg.content) + 4
78+
(msg) => tokenCount(msg.content) + 4
8079
).reduce((a, b) => a + b, 0);
8180

82-
const MAX_REQ_TOKENS = 3000 - INIT_MESSAGES_PROMPT_LENGTH;
81+
const ADJUSTMENT_FACTOR = 20;
8382

8483
export const generateCommitMessageByDiff = async (
85-
diff: string
84+
diff: string
8685
): Promise<string> => {
87-
try {
88-
if (tokenCount(diff) >= MAX_REQ_TOKENS) {
89-
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
90-
diff,
91-
MAX_REQ_TOKENS
92-
);
93-
94-
const commitMessages = await Promise.all(commitMessagePromises);
95-
96-
return commitMessages.join('\n\n');
97-
} else {
98-
const messages = generateCommitMessageChatCompletionPrompt(diff);
99-
100-
const commitMessage = await api.generateCommitMessage(messages);
101-
102-
if (!commitMessage)
103-
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
104-
105-
return commitMessage;
86+
try {
87+
const MAX_REQUEST_TOKENS = DEFAULT_MODEL_TOKEN_LIMIT
88+
- ADJUSTMENT_FACTOR
89+
- INIT_MESSAGES_PROMPT_LENGTH
90+
- config?.OCO_OPENAI_MAX_TOKENS;
91+
92+
if (tokenCount(diff) >= MAX_REQUEST_TOKENS) {
93+
const commitMessagePromises = getCommitMsgsPromisesFromFileDiffs(
94+
diff,
95+
MAX_REQUEST_TOKENS
96+
);
97+
98+
const commitMessages = [];
99+
for (const promise of commitMessagePromises) {
100+
commitMessages.push(await promise);
101+
await delay(2000);
102+
}
103+
104+
return commitMessages.join('\n\n');
105+
} else {
106+
const messages = generateCommitMessageChatCompletionPrompt(diff);
107+
108+
const commitMessage = await api.generateCommitMessage(messages);
109+
110+
if (!commitMessage)
111+
throw new Error(GenerateCommitMessageErrorEnum.emptyMessage);
112+
113+
return commitMessage;
114+
}
115+
} catch (error) {
116+
throw error;
106117
}
107-
} catch (error) {
108-
throw error;
109-
}
110118
};
111119

112120
function getMessagesPromisesByChangesInFile(
113-
fileDiff: string,
114-
separator: string,
115-
maxChangeLength: number
121+
fileDiff: string,
122+
separator: string,
123+
maxChangeLength: number
116124
) {
117-
const hunkHeaderSeparator = '@@ ';
118-
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
119-
120-
// merge multiple line-diffs into 1 to save tokens
121-
const mergedChanges = mergeDiffs(
122-
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
123-
maxChangeLength
124-
);
125-
126-
const lineDiffsWithHeader = mergedChanges.map(
127-
(change) => fileHeader + change
128-
);
129-
130-
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
131-
const messages = generateCommitMessageChatCompletionPrompt(
132-
separator + lineDiff
125+
const hunkHeaderSeparator = '@@ ';
126+
const [fileHeader, ...fileDiffByLines] = fileDiff.split(hunkHeaderSeparator);
127+
128+
// merge multiple line-diffs into 1 to save tokens
129+
const mergedChanges = mergeDiffs(
130+
fileDiffByLines.map((line) => hunkHeaderSeparator + line),
131+
maxChangeLength
133132
);
134133

135-
return api.generateCommitMessage(messages);
136-
});
134+
const lineDiffsWithHeader = [];
135+
for (const change of mergedChanges) {
136+
const totalChange = fileHeader + change;
137+
if (tokenCount(totalChange) > maxChangeLength) {
138+
// If the totalChange is too large, split it into smaller pieces
139+
const splitChanges = splitDiff(totalChange, maxChangeLength);
140+
lineDiffsWithHeader.push(...splitChanges);
141+
} else {
142+
lineDiffsWithHeader.push(totalChange);
143+
}
144+
}
145+
146+
const commitMsgsFromFileLineDiffs = lineDiffsWithHeader.map((lineDiff) => {
147+
const messages = generateCommitMessageChatCompletionPrompt(
148+
separator + lineDiff
149+
);
150+
151+
return api.generateCommitMessage(messages);
152+
});
137153

138-
return commitMsgsFromFileLineDiffs;
154+
return commitMsgsFromFileLineDiffs;
155+
}
156+
157+
158+
function splitDiff(diff: string, maxChangeLength: number) {
159+
const lines = diff.split('\n');
160+
const splitDiffs = [];
161+
let currentDiff = '';
162+
163+
for (let line of lines) {
164+
// If a single line exceeds maxChangeLength, split it into multiple lines
165+
while (tokenCount(line) > maxChangeLength) {
166+
const subLine = line.substring(0, maxChangeLength);
167+
line = line.substring(maxChangeLength);
168+
splitDiffs.push(subLine);
169+
}
170+
171+
// Check the tokenCount of the currentDiff and the line separately
172+
if (tokenCount(currentDiff) + tokenCount('\n' + line) > maxChangeLength) {
173+
// If adding the next line would exceed the maxChangeLength, start a new diff
174+
splitDiffs.push(currentDiff);
175+
currentDiff = line;
176+
} else {
177+
// Otherwise, add the line to the current diff
178+
currentDiff += '\n' + line;
179+
}
180+
}
181+
182+
// Add the last diff
183+
if (currentDiff) {
184+
splitDiffs.push(currentDiff);
185+
}
186+
187+
return splitDiffs;
139188
}
140189

141190
export function getCommitMsgsPromisesFromFileDiffs(
142-
diff: string,
143-
maxDiffLength: number
191+
diff: string,
192+
maxDiffLength: number
144193
) {
145-
const separator = 'diff --git ';
194+
const separator = 'diff --git ';
146195

147-
const diffByFiles = diff.split(separator).slice(1);
196+
const diffByFiles = diff.split(separator).slice(1);
148197

149-
// merge multiple files-diffs into 1 prompt to save tokens
150-
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
198+
// merge multiple files-diffs into 1 prompt to save tokens
199+
const mergedFilesDiffs = mergeDiffs(diffByFiles, maxDiffLength);
151200

152-
const commitMessagePromises = [];
201+
const commitMessagePromises = [];
153202

154-
for (const fileDiff of mergedFilesDiffs) {
155-
if (tokenCount(fileDiff) >= maxDiffLength) {
156-
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
157-
const messagesPromises = getMessagesPromisesByChangesInFile(
158-
fileDiff,
159-
separator,
160-
maxDiffLength
161-
);
203+
for (const fileDiff of mergedFilesDiffs) {
204+
if (tokenCount(fileDiff) >= maxDiffLength) {
205+
// if file-diff is bigger than gpt context — split fileDiff into lineDiff
206+
const messagesPromises = getMessagesPromisesByChangesInFile(
207+
fileDiff,
208+
separator,
209+
maxDiffLength
210+
);
162211

163-
commitMessagePromises.push(...messagesPromises);
164-
} else {
165-
const messages = generateCommitMessageChatCompletionPrompt(
166-
separator + fileDiff
167-
);
212+
commitMessagePromises.push(...messagesPromises);
213+
} else {
214+
const messages = generateCommitMessageChatCompletionPrompt(
215+
separator + fileDiff
216+
);
168217

169-
commitMessagePromises.push(api.generateCommitMessage(messages));
218+
commitMessagePromises.push(api.generateCommitMessage(messages));
219+
}
170220
}
171-
}
172-
return commitMessagePromises;
221+
222+
223+
return commitMessagePromises;
224+
}
225+
226+
function delay(ms: number) {
227+
return new Promise(resolve => setTimeout(resolve, ms));
173228
}

0 commit comments

Comments
 (0)