diff --git a/src/commands/aicommits.ts b/src/commands/aicommits.ts index e76739cb..2cc88f33 100644 --- a/src/commands/aicommits.ts +++ b/src/commands/aicommits.ts @@ -52,19 +52,29 @@ export default async ( ); const { env } = process; - const config = await getConfig({ - 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(), - type: commitType?.toString(), - }); + const config = await getConfig( + { + OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY, + authHeaderName: env.authHeaderName, + hostname: env.hostname, + apipath: env.apipath, + proxy: + env.https_proxy || + env.HTTPS_PROXY || + env.http_proxy || + env.HTTP_PROXY, + generate: generate?.toString(), + type: commitType?.toString(), + }, + false + ); const s = spinner(); s.start('The AI is analyzing your changes'); let messages: string[]; try { messages = await generateCommitMessage( + config.authHeaderName, config.OPENAI_KEY, config.model, config.locale, @@ -73,6 +83,8 @@ export default async ( config['max-length'], config.type, config.timeout, + config.hostname, + config.apipath, config.proxy ); } finally { diff --git a/src/commands/config.ts b/src/commands/config.ts index 408cf64c..a87be992 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -25,7 +25,10 @@ export default command( if (mode === 'set') { await setConfigs( - keyValues.map((keyValue) => keyValue.split('=') as [string, string]) + keyValues.map((keyValue) => { + const equals = keyValue.indexOf('=') + return [keyValue.substring(0,equals), keyValue.substring(equals+1)] + }) ); return; } diff --git a/src/commands/prepare-commit-msg-hook.ts b/src/commands/prepare-commit-msg-hook.ts index 234b6872..e40b2db6 100644 --- a/src/commands/prepare-commit-msg-hook.ts +++ b/src/commands/prepare-commit-msg-hook.ts @@ -40,6 +40,7 @@ export default () => let messages: string[]; try { messages = await generateCommitMessage( + config.authHeaderName, config.OPENAI_KEY, config.model, config.locale, @@ -48,6 +49,8 @@ export default () => config['max-length'], config.type, config.timeout, + config.hostname, + config.apipath, config.proxy ); } finally { diff --git a/src/utils/config.ts b/src/utils/config.ts index 0e07a59f..3f4aba5a 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -27,8 +27,6 @@ const configParsers = { 'Please set your OpenAI API key via `aicommits config set OPENAI_KEY=`' ); } - parseAssert('OPENAI_KEY', key.startsWith('sk-'), 'Must start with "sk-"'); - // Key can range from 43~51 characters. There's no spec to assert this. return key; }, @@ -115,6 +113,33 @@ const configParsers = { return parsed; }, + authHeaderName(authHeaderName?: string) { + if (!authHeaderName) { + return 'Authorization'; + } + + return authHeaderName; + }, + hostname(hostname?: string) { + if (!hostname) { + return 'api.openai.com'; + } + + parseAssert( + 'hostname', + !/^http/i.test(hostname) && !/\/$/.test(hostname), + 'Do not include protocol or path in hostname, only hostname' + ); + + return hostname; + }, + apipath(apipath?: string) { + if (!apipath) { + return '/v1/chat/completions'; + } + + return apipath; + }, } as const; type ConfigKeys = keyof typeof configParsers; diff --git a/src/utils/openai.ts b/src/utils/openai.ts index d8c767ed..3963bc23 100644 --- a/src/utils/openai.ts +++ b/src/utils/openai.ts @@ -15,7 +15,7 @@ import { generatePrompt } from './prompt.js'; const httpsPost = async ( hostname: string, - path: string, + apipath: string, headers: Record, json: unknown, timeout: number, @@ -31,7 +31,7 @@ const httpsPost = async ( { port: 443, hostname, - path, + path: apipath, method: 'POST', headers: { ...headers, @@ -68,17 +68,21 @@ const httpsPost = async ( }); const createChatCompletion = async ( + authHeaderName: string, apiKey: string, json: CreateChatCompletionRequest, timeout: number, + hostname: string, + apipath: string, proxy?: string ) => { + let headers: any = {}; + headers[authHeaderName] = `${apiKey}`; + const { response, data } = await httpsPost( - 'api.openai.com', - '/v1/chat/completions', - { - Authorization: `Bearer ${apiKey}`, - }, + hostname, + apipath, + headers, json, timeout, proxy @@ -131,6 +135,7 @@ const deduplicateMessages = (array: string[]) => Array.from(new Set(array)); // }; export const generateCommitMessage = async ( + authHeaderName: string, apiKey: string, model: TiktokenModel, locale: string, @@ -139,10 +144,13 @@ export const generateCommitMessage = async ( maxLength: number, type: CommitType, timeout: number, + hostname: string, + apipath: string, proxy?: string ) => { try { const completion = await createChatCompletion( + authHeaderName, apiKey, { model, @@ -165,6 +173,8 @@ export const generateCommitMessage = async ( n: completions, }, timeout, + hostname, + apipath, proxy ); diff --git a/tests/specs/config.ts b/tests/specs/config.ts index 9bfe45fc..bd48ea34 100644 --- a/tests/specs/config.ts +++ b/tests/specs/config.ts @@ -17,16 +17,6 @@ export default testSuite(({ describe }) => { expect(stderr).toMatch('Invalid config property: UNKNOWN'); }); - test('set invalid OPENAI_KEY', async () => { - const { stderr } = await aicommits(['config', 'set', 'OPENAI_KEY=abc'], { - reject: false, - }); - - expect(stderr).toMatch( - 'Invalid config property OPENAI_KEY: Must start with "sk-"' - ); - }); - await test('set config file', async () => { await aicommits(['config', 'set', openAiToken]); @@ -106,6 +96,33 @@ export default testSuite(({ describe }) => { }); }); + await describe('hostname', ({ test }) => { + test('must be an hostname', async () => { + const { stderr } = await aicommits( + ['config', 'set', 'hostname=https://api.openai.com/'], + { reject: false } + ); + + expect(stderr).toMatch( + 'Do not include protocol or path in hostname, only hostname' + ); + }); + + test('updates config', async () => { + const defaultConfig = await aicommits(['config', 'get', 'hostname']); + expect(defaultConfig.stdout).toBe('hostname=api.openai.com'); + + const hostname = 'hostname=api.chatanywhere.com.cn'; + await aicommits(['config', 'set', hostname]); + + const configFile = await fs.readFile(configPath, 'utf8'); + expect(configFile).toMatch(hostname); + + const get = await aicommits(['config', 'get', 'hostname']); + expect(get.stdout).toBe(hostname); + }); + }); + await test('set config file', async () => { await aicommits(['config', 'set', openAiToken]); diff --git a/tests/specs/openai/conventional-commits.ts b/tests/specs/openai/conventional-commits.ts index 7a3e43e6..c539c0f7 100644 --- a/tests/specs/openai/conventional-commits.ts +++ b/tests/specs/openai/conventional-commits.ts @@ -3,7 +3,7 @@ import { generateCommitMessage } from '../../../src/utils/openai.js'; import type { ValidConfig } from '../../../src/utils/config.js'; import { getDiff } from '../../utils.js'; -const { OPENAI_KEY } = process.env; +const { OPENAI_KEY, authHeaderName, hostname, apipath } = process.env; export default testSuite(({ describe }) => { if (!OPENAI_KEY) { @@ -139,6 +139,7 @@ export default testSuite(({ describe }) => { ...configOverrides, } as ValidConfig; const commitMessages = await generateCommitMessage( + authHeaderName ?? 'Authorization', OPENAI_KEY!, 'gpt-3.5-turbo', config.locale, @@ -146,7 +147,9 @@ export default testSuite(({ describe }) => { config.generate, config['max-length'], config.type, - 7000 + 7000, + hostname ?? 'api.openai.com', + apipath ?? '/v1/completions' ); return commitMessages[0];