diff --git a/README.md b/README.md index 6591805..faeaaa1 100644 --- a/README.md +++ b/README.md @@ -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. -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 diff --git a/package.json b/package.json index e1fd30b..370948a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "eslint": "^8.35.0", "execa": "^7.0.0", "fs-fixture": "^1.2.0", + "https-proxy-agent": "^5.0.1", "ini": "^3.0.1", "kolorist": "^1.7.0", "lint-staged": "^13.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16aec88..cf51dd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,7 @@ specifiers: eslint: ^8.35.0 execa: ^7.0.0 fs-fixture: ^1.2.0 + https-proxy-agent: ^5.0.1 ini: ^3.0.1 kolorist: ^1.7.0 lint-staged: ^13.1.2 @@ -41,6 +42,7 @@ devDependencies: eslint: 8.35.0 execa: 7.0.0 fs-fixture: 1.2.0 + https-proxy-agent: 5.0.1 ini: 3.0.1 kolorist: 1.7.0 lint-staged: 13.1.2 @@ -787,6 +789,15 @@ packages: hasBin: 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: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -2074,6 +2085,16 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} 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: resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} engines: {node: '>=12.20.0'} diff --git a/src/commands/aicommits.ts b/src/commands/aicommits.ts index 2ca21b7..bb51adf 100644 --- a/src/commands/aicommits.ts +++ b/src/commands/aicommits.ts @@ -35,8 +35,10 @@ export default async ( staged.files.map(file => ` ${file}`).join('\n') }`); + const { env } = process; 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(), }); @@ -49,6 +51,7 @@ export default async ( config.locale, staged.diff, config.generate, + config.proxy, ); } finally { s.stop('Changes analyzed'); diff --git a/src/commands/prepare-commit-msg-hook.ts b/src/commands/prepare-commit-msg-hook.ts index 57d3ae8..5179902 100644 --- a/src/commands/prepare-commit-msg-hook.ts +++ b/src/commands/prepare-commit-msg-hook.ts @@ -30,7 +30,10 @@ export default () => (async () => { 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(); s.start('The AI is analyzing your changes'); @@ -41,6 +44,7 @@ export default () => (async () => { config.locale, staged!.diff, config.generate, + config.proxy, ); } finally { s.stop('Changes analyzed'); diff --git a/src/utils/config.ts b/src/utils/config.ts index d029e9a..d3e0bef 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -51,6 +51,15 @@ const configParsers = { 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; type ConfigKeys = keyof typeof configParsers; diff --git a/src/utils/openai.ts b/src/utils/openai.ts index 17cff90..59682c0 100644 --- a/src/utils/openai.ts +++ b/src/utils/openai.ts @@ -2,6 +2,7 @@ import https from 'https'; import type { ClientRequest, IncomingMessage } from 'http'; import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai'; import { encoding_for_model as encodingForModel } from '@dqbd/tiktoken'; +import createHttpsProxyAgent from 'https-proxy-agent'; import { KnownError } from './error.js'; const httpsPost = async ( @@ -9,6 +10,7 @@ const httpsPost = async ( path: string, headers: Record, json: unknown, + proxy?: string, ) => new Promise<{ request: ClientRequest; response: IncomingMessage; @@ -27,6 +29,11 @@ const httpsPost = async ( 'Content-Length': Buffer.byteLength(postContent), }, timeout: 10_000, // 10s + agent: ( + proxy + ? createHttpsProxyAgent(proxy) + : undefined + ), }, (response) => { const body: Buffer[] = []; @@ -53,6 +60,7 @@ const httpsPost = async ( const createChatCompletion = async ( apiKey: string, json: CreateChatCompletionRequest, + proxy?: string, ) => { const { response, data } = await httpsPost( 'api.openai.com', @@ -61,6 +69,7 @@ const createChatCompletion = async ( Authorization: `Bearer ${apiKey}`, }, json, + proxy, ); if ( @@ -97,6 +106,7 @@ export const generateCommitMessage = async ( locale: string, diff: string, completions: number, + proxy?: string, ) => { const prompt = getPrompt(locale, diff); @@ -109,20 +119,24 @@ export const generateCommitMessage = async ( } 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, - }); + 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