feat: request timeout config (#191)

Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
This commit is contained in:
Rocktim
2023-04-08 17:54:47 +05:30
committed by GitHub
parent 75eee29a4e
commit 42a2a39f6f
7 changed files with 79 additions and 7 deletions

View File

@@ -74,10 +74,6 @@ aicommits --generate <i> # or -g <i>
> Warning: this uses more tokens, meaning it costs more. > Warning: this uses more tokens, meaning it costs more.
```sh
aicommits --all
```
### Git hook ### Git hook
You can also integrate _aicommits_ with Git via the [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. This lets you use Git like you normally would, and edit the commit message before committing. You can also integrate _aicommits_ with Git via the [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. This lets you use Git like you normally would, and edit the commit message before committing.
@@ -189,6 +185,15 @@ The Chat Completions (`/v1/chat/completions`) model to use. Consult the list of
> Tip: If you have access, try upgrading to [`gpt-4`](https://platform.openai.com/docs/models/gpt-4) for next-level code analysis. It can handle double the input size, but comes at a higher cost. Check out OpenAI's website to learn more. > Tip: If you have access, try upgrading to [`gpt-4`](https://platform.openai.com/docs/models/gpt-4) for next-level code analysis. It can handle double the input size, but comes at a higher cost. Check out OpenAI's website to learn more.
#### timeout
The timeout for network requests to the OpenAI API in milliseconds.
Default: `10000` (10 seconds)
```sh
aicommits config set timeout=20000 # 20s
```
## How it works ## How it works
This CLI tool runs `git diff` to grab all your latest code changes, sends them to OpenAI's GPT-3, then returns the AI generated commit message. This CLI tool runs `git diff` to grab all your latest code changes, sends them to OpenAI's GPT-3, then returns the AI generated commit message.

View File

@@ -58,6 +58,7 @@ export default async (
config.locale, config.locale,
staged.diff, staged.diff,
config.generate, config.generate,
config.timeout,
config.proxy, config.proxy,
); );
} finally { } finally {

View File

@@ -45,6 +45,7 @@ export default () => (async () => {
config.locale, config.locale,
staged!.diff, staged!.diff,
config.generate, config.generate,
config.timeout,
config.proxy, config.proxy,
); );
} finally { } finally {

View File

@@ -68,6 +68,18 @@ const configParsers = {
return model as TiktokenModel; return model as TiktokenModel;
}, },
timeout(timeout?: string) {
if (!timeout) {
return 10_000;
}
parseAssert('timeout', /^\d+$/.test(timeout), 'Must be an integer');
const parsed = Number(timeout);
parseAssert('timeout', parsed >= 500, 'Must be greater than 500ms');
return parsed;
},
} as const; } as const;
type ConfigKeys = keyof typeof configParsers; type ConfigKeys = keyof typeof configParsers;

View File

@@ -10,6 +10,7 @@ const httpsPost = async (
path: string, path: string,
headers: Record<string, string>, headers: Record<string, string>,
json: unknown, json: unknown,
timeout: number,
proxy?: string, proxy?: string,
) => new Promise<{ ) => new Promise<{
request: ClientRequest; request: ClientRequest;
@@ -28,7 +29,7 @@ const httpsPost = async (
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postContent), 'Content-Length': Buffer.byteLength(postContent),
}, },
timeout: 10_000, // 10s timeout,
agent: ( agent: (
proxy proxy
? createHttpsProxyAgent(proxy) ? createHttpsProxyAgent(proxy)
@@ -50,7 +51,7 @@ const httpsPost = async (
request.on('error', reject); request.on('error', reject);
request.on('timeout', () => { request.on('timeout', () => {
request.destroy(); request.destroy();
reject(new KnownError('Request timed out')); reject(new KnownError(`Time out error: request took over ${timeout}ms. Try increasing the \`timeout\` config, or checking the OpenAI API status https://status.openai.com`));
}); });
request.write(postContent); request.write(postContent);
@@ -60,6 +61,7 @@ const httpsPost = async (
const createChatCompletion = async ( const createChatCompletion = async (
apiKey: string, apiKey: string,
json: CreateChatCompletionRequest, json: CreateChatCompletionRequest,
timeout: number,
proxy?: string, proxy?: string,
) => { ) => {
const { response, data } = await httpsPost( const { response, data } = await httpsPost(
@@ -69,6 +71,7 @@ const createChatCompletion = async (
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
}, },
json, json,
timeout,
proxy, proxy,
); );
@@ -105,6 +108,7 @@ export const generateCommitMessage = async (
locale: string, locale: string,
diff: string, diff: string,
completions: number, completions: number,
timeout: number,
proxy?: string, proxy?: string,
) => { ) => {
const prompt = getPrompt(locale, diff); const prompt = getPrompt(locale, diff);
@@ -126,6 +130,7 @@ export const generateCommitMessage = async (
stream: false, stream: false,
n: completions, n: completions,
}, },
timeout,
proxy, proxy,
); );

View File

@@ -252,5 +252,34 @@ export default testSuite(({ describe }) => {
await fixture.rm(); await fixture.rm();
}); });
}); });
test('Fails on timeout', async () => {
const { fixture, aicommits } = await createFixture({
...files,
'.aicommits': `${files['.aicommits']}\ntimeout=500`,
});
const git = await createGit(fixture.path);
await git('add', ['data.json']);
const committing = aicommits([], {
reject: false,
});
committing.stdout!.on('data', (buffer: Buffer) => {
const stdout = buffer.toString();
if (stdout.match('└')) {
committing.stdin!.write('y');
committing.stdin!.end();
}
});
const { stdout, exitCode } = await committing;
expect(exitCode).toBe(1);
expect(stdout).toMatch('Time out error: request took over 500ms.');
await fixture.rm();
});
}); });
}); });

View File

@@ -4,7 +4,7 @@ import { testSuite, expect } from 'manten';
import { createFixture } from '../utils.js'; import { createFixture } from '../utils.js';
export default testSuite(({ describe }) => { export default testSuite(({ describe }) => {
describe('config', async ({ test }) => { describe('config', async ({ test, describe }) => {
const { fixture, aicommits } = await createFixture(); const { fixture, aicommits } = await createFixture();
const configPath = path.join(fixture.path, '.aicommits'); const configPath = path.join(fixture.path, '.aicommits');
const openAiToken = 'OPENAI_KEY=sk-abc'; const openAiToken = 'OPENAI_KEY=sk-abc';
@@ -49,6 +49,25 @@ export default testSuite(({ describe }) => {
expect(stderr).toBe(''); expect(stderr).toBe('');
}); });
await describe('timeout', ({ test }) => {
test('setting invalid timeout config', async () => {
const { stderr } = await aicommits(['config', 'set', 'timeout=abc'], {
reject: false,
});
expect(stderr).toMatch('Must be an integer');
});
test('setting valid timeout config', async () => {
const timeout = 'timeout=20000';
await aicommits(['config', 'set', timeout]);
const configFile = await fs.readFile(configPath, 'utf8');
expect(configFile).toMatch(timeout);
});
});
await fixture.rm(); await fixture.rm();
}); });
}); });