diff --git a/README.md b/README.md index c07a9f2e..1ea9b84f 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,14 @@ Required The OpenAI API key. You can retrieve it from [OpenAI API Keys page](https://platform.openai.com/account/api-keys). +For local OpenAI-compatible API server an empty key should be specified as `no_api_key`. + +#### OPENAI_URL + +Optional + +The OpenAI server URL. Defaults to `https://api.openai.com`. Both `https` and `http` protocols supported. + #### locale Default: `en` diff --git a/src/commands/aicommits.ts b/src/commands/aicommits.ts index e76739cb..80b83d7d 100644 --- a/src/commands/aicommits.ts +++ b/src/commands/aicommits.ts @@ -65,6 +65,7 @@ export default async ( let messages: string[]; try { messages = await generateCommitMessage( + config.OPENAI_URL, config.OPENAI_KEY, config.model, config.locale, diff --git a/src/utils/config.ts b/src/utils/config.ts index 0e07a59f..33df5236 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -27,7 +27,7 @@ const configParsers = { 'Please set your OpenAI API key via `aicommits config set OPENAI_KEY=`' ); } - parseAssert('OPENAI_KEY', key.startsWith('sk-'), 'Must start with "sk-"'); + parseAssert('OPENAI_KEY', /^((sk-|no_api_key){1}[a-zA-Z0-9]*)$/.test(key), 'Must start with "sk-" or "no_api_key"'); // Key can range from 43~51 characters. There's no spec to assert this. return key; @@ -115,6 +115,15 @@ const configParsers = { return parsed; }, + OPENAI_URL(url?: string) { + if (!url || url.length === 0) { + return undefined; + } + + parseAssert('OPENAI_URL', /^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 d8c767ed..ac39e4c0 100644 --- a/src/utils/openai.ts +++ b/src/utils/openai.ts @@ -1,4 +1,5 @@ import https from 'https'; +import http from 'http'; import type { ClientRequest, IncomingMessage } from 'http'; import type { CreateChatCompletionRequest, @@ -14,7 +15,7 @@ import type { CommitType } from './config.js'; import { generatePrompt } from './prompt.js'; const httpsPost = async ( - hostname: string, + url: URL, path: string, headers: Record, json: unknown, @@ -27,10 +28,14 @@ const httpsPost = async ( data: string; }>((resolve, reject) => { const postContent = JSON.stringify(json); - const request = https.request( + var connector = https; + if (url.protocol != 'https') { + connector = http; + } + const request = connector.request( { - port: 443, - hostname, + hostname: url.hostname, + port: Number(url.port || '443'), path, method: 'POST', headers: { @@ -68,13 +73,18 @@ const httpsPost = async ( }); const createChatCompletion = async ( + openai_url: string, apiKey: string, json: CreateChatCompletionRequest, timeout: number, proxy?: string ) => { + if (!openai_url || openai_url.length === 0) { + openai_url = 'https://api.openai.com/'; + } + const url = new URL(openai_url); const { response, data } = await httpsPost( - 'api.openai.com', + url, '/v1/chat/completions', { Authorization: `Bearer ${apiKey}`, @@ -131,6 +141,7 @@ const deduplicateMessages = (array: string[]) => Array.from(new Set(array)); // }; export const generateCommitMessage = async ( + openai_url: string, apiKey: string, model: TiktokenModel, locale: string, @@ -143,6 +154,7 @@ export const generateCommitMessage = async ( ) => { try { const completion = await createChatCompletion( + openai_url, apiKey, { model,