feat: HTTP + HTTPS proxy support (#139)

Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
This commit is contained in:
Sam Büth
2023-03-27 08:51:51 +02:00
committed by GitHub
parent 6e02dc8f81
commit a0db0f3ece
7 changed files with 77 additions and 17 deletions

View File

@@ -165,7 +165,15 @@ The number of commit messages to generate to pick from.
Note, this will use more tokens as it generates more results. Note, this will use more tokens as it generates more results.
This can also be configured with the CLI flag `--generate`. #### proxy
Set a HTTP/HTTPS proxy to use for requests.
To clear the proxy option, you can use the command (note the empty value after the equals sign):
```sh
aicommits config set proxy=
```
## How it works ## How it works

View File

@@ -46,6 +46,7 @@
"eslint": "^8.35.0", "eslint": "^8.35.0",
"execa": "^7.0.0", "execa": "^7.0.0",
"fs-fixture": "^1.2.0", "fs-fixture": "^1.2.0",
"https-proxy-agent": "^5.0.1",
"ini": "^3.0.1", "ini": "^3.0.1",
"kolorist": "^1.7.0", "kolorist": "^1.7.0",
"lint-staged": "^13.1.2", "lint-staged": "^13.1.2",

21
pnpm-lock.yaml generated
View File

@@ -17,6 +17,7 @@ specifiers:
eslint: ^8.35.0 eslint: ^8.35.0
execa: ^7.0.0 execa: ^7.0.0
fs-fixture: ^1.2.0 fs-fixture: ^1.2.0
https-proxy-agent: ^5.0.1
ini: ^3.0.1 ini: ^3.0.1
kolorist: ^1.7.0 kolorist: ^1.7.0
lint-staged: ^13.1.2 lint-staged: ^13.1.2
@@ -41,6 +42,7 @@ devDependencies:
eslint: 8.35.0 eslint: 8.35.0
execa: 7.0.0 execa: 7.0.0
fs-fixture: 1.2.0 fs-fixture: 1.2.0
https-proxy-agent: 5.0.1
ini: 3.0.1 ini: 3.0.1
kolorist: 1.7.0 kolorist: 1.7.0
lint-staged: 13.1.2 lint-staged: 13.1.2
@@ -787,6 +789,15 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/agent-base/6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/aggregate-error/3.1.0: /aggregate-error/3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2074,6 +2085,16 @@ packages:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true dev: true
/https-proxy-agent/5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/human-signals/3.0.1: /human-signals/3.0.1:
resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}

View File

@@ -35,8 +35,10 @@ export default async (
staged.files.map(file => ` ${file}`).join('\n') staged.files.map(file => ` ${file}`).join('\n')
}`); }`);
const { env } = process;
const config = await getConfig({ const config = await getConfig({
OPENAI_KEY: process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY, OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY,
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
generate: generate?.toString(), generate: generate?.toString(),
}); });
@@ -49,6 +51,7 @@ export default async (
config.locale, config.locale,
staged.diff, staged.diff,
config.generate, config.generate,
config.proxy,
); );
} finally { } finally {
s.stop('Changes analyzed'); s.stop('Changes analyzed');

View File

@@ -30,7 +30,10 @@ export default () => (async () => {
intro(bgCyan(black(' aicommits '))); intro(bgCyan(black(' aicommits ')));
const config = await getConfig(); const { env } = process;
const config = await getConfig({
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
});
const s = spinner(); const s = spinner();
s.start('The AI is analyzing your changes'); s.start('The AI is analyzing your changes');
@@ -41,6 +44,7 @@ export default () => (async () => {
config.locale, config.locale,
staged!.diff, staged!.diff,
config.generate, config.generate,
config.proxy,
); );
} finally { } finally {
s.stop('Changes analyzed'); s.stop('Changes analyzed');

View File

@@ -51,6 +51,15 @@ const configParsers = {
return parsed; return parsed;
}, },
proxy(url?: string) {
if (!url || url.length === 0) {
return undefined;
}
parseAssert('proxy', /^https?:\/\//.test(url), 'Must be a valid URL');
return url;
},
} as const; } as const;
type ConfigKeys = keyof typeof configParsers; type ConfigKeys = keyof typeof configParsers;

View File

@@ -2,6 +2,7 @@ import https from 'https';
import type { ClientRequest, IncomingMessage } from 'http'; import type { ClientRequest, IncomingMessage } from 'http';
import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai'; import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
import { encoding_for_model as encodingForModel } from '@dqbd/tiktoken'; import { encoding_for_model as encodingForModel } from '@dqbd/tiktoken';
import createHttpsProxyAgent from 'https-proxy-agent';
import { KnownError } from './error.js'; import { KnownError } from './error.js';
const httpsPost = async ( const httpsPost = async (
@@ -9,6 +10,7 @@ const httpsPost = async (
path: string, path: string,
headers: Record<string, string>, headers: Record<string, string>,
json: unknown, json: unknown,
proxy?: string,
) => new Promise<{ ) => new Promise<{
request: ClientRequest; request: ClientRequest;
response: IncomingMessage; response: IncomingMessage;
@@ -27,6 +29,11 @@ const httpsPost = async (
'Content-Length': Buffer.byteLength(postContent), 'Content-Length': Buffer.byteLength(postContent),
}, },
timeout: 10_000, // 10s timeout: 10_000, // 10s
agent: (
proxy
? createHttpsProxyAgent(proxy)
: undefined
),
}, },
(response) => { (response) => {
const body: Buffer[] = []; const body: Buffer[] = [];
@@ -53,6 +60,7 @@ const httpsPost = async (
const createChatCompletion = async ( const createChatCompletion = async (
apiKey: string, apiKey: string,
json: CreateChatCompletionRequest, json: CreateChatCompletionRequest,
proxy?: string,
) => { ) => {
const { response, data } = await httpsPost( const { response, data } = await httpsPost(
'api.openai.com', 'api.openai.com',
@@ -61,6 +69,7 @@ const createChatCompletion = async (
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
}, },
json, json,
proxy,
); );
if ( if (
@@ -97,6 +106,7 @@ export const generateCommitMessage = async (
locale: string, locale: string,
diff: string, diff: string,
completions: number, completions: number,
proxy?: string,
) => { ) => {
const prompt = getPrompt(locale, diff); const prompt = getPrompt(locale, diff);
@@ -109,20 +119,24 @@ export const generateCommitMessage = async (
} }
try { try {
const completion = await createChatCompletion(apiKey, { const completion = await createChatCompletion(
model, apiKey,
messages: [{ {
role: 'user', model,
content: prompt, messages: [{
}], role: 'user',
temperature: 0.7, content: prompt,
top_p: 1, }],
frequency_penalty: 0, temperature: 0.7,
presence_penalty: 0, top_p: 1,
max_tokens: 200, frequency_penalty: 0,
stream: false, presence_penalty: 0,
n: completions, max_tokens: 200,
}); stream: false,
n: completions,
},
proxy,
);
return deduplicateMessages( return deduplicateMessages(
completion.choices completion.choices