From 6519441289788c3ed5e8706c4ef4c0e6dd687f08 Mon Sep 17 00:00:00 2001
From: Takuya Ono <takuya-o@users.osdn.me>
Date: Sun, 7 May 2023 03:24:14 +0900
Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20feat(api.ts):=20add=20support?=
 =?UTF-8?q?=20for=20Azure=20OpenAI=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The Azure OpenAI API is now supported in addition to the OpenAI API. The API type can be specified in the configuration file using the OPENAI_API_TYPE key. If the key is not specified, the default value is 'openai'. The AzureOpenAIApi class is added to the utils folder to handle the Azure OpenAI API calls. The createChatCompletion method is implemented in the AzureOpenAIApi class to handle the chat completion requests. The method is called in the generateCommitMessage method in the OpenAi class if the apiType is set to 'azure'.
---
 README.md                | 14 ++++++++
 src/api.ts               | 10 ++++--
 src/commands/config.ts   | 22 ++++++++++--
 src/utils/AzureOpenAI.ts | 77 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 118 insertions(+), 5 deletions(-)
 create mode 100644 src/utils/AzureOpenAI.ts

diff --git a/README.md b/README.md
index 4443a734..657d18d5 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,20 @@ oc
 
 ## Features
 
+### Switch to Azure OpenAI
+
+By default OpenCommit uses [OpenAI](https://openai.com).
+
+You could switch to [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/)🚀
+
+```sh
+opencommit config set OPENAI_API_TYPE=azure
+```
+
+Of course need to set 'OPENAI_API_KEY'. And also need to set the
+'OPENAI_BASE_PATH' for the endpoint and set the deployment name to
+'model'.
+
 ### Switch to GPT-4
 
 By default OpenCommit uses GPT-3.5-turbo (ChatGPT).
diff --git a/src/api.ts b/src/api.ts
index 3b387334..4854e4fb 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -6,6 +6,9 @@ import {
   Configuration as OpenAiApiConfiguration,
   OpenAIApi
 } from 'openai';
+import {
+  AzureOpenAIApi
+} from './utils/AzureOpenAI';
 
 import { CONFIG_MODES, getConfig } from './commands/config';
 
@@ -14,6 +17,7 @@ const config = getConfig();
 let apiKey = config?.OPENAI_API_KEY;
 let basePath = config?.OPENAI_BASE_PATH;
 let maxTokens = config?.OPENAI_MAX_TOKENS;
+let apiType = config?.OPENAI_API_TYPE || 'openai';
 
 const [command, mode] = process.argv.slice(2);
 
@@ -36,13 +40,15 @@ class OpenAi {
   private openAiApiConfiguration = new OpenAiApiConfiguration({
     apiKey: apiKey
   });
-  private openAI!: OpenAIApi;
+  private openAI!: OpenAIApi | AzureOpenAIApi;
 
   constructor() {
     if (basePath) {
       this.openAiApiConfiguration.basePath = basePath;
     }
-    this.openAI = new OpenAIApi(this.openAiApiConfiguration);
+    this.openAI = apiType === 'azure'
+    ? new AzureOpenAIApi(this.openAiApiConfiguration)
+    : new OpenAIApi(this.openAiApiConfiguration);
   }
 
   public generateCommitMessage = async (
diff --git a/src/commands/config.ts b/src/commands/config.ts
index 9f160348..974ab3d0 100644
--- a/src/commands/config.ts
+++ b/src/commands/config.ts
@@ -12,6 +12,7 @@ export enum CONFIG_KEYS {
   OPENAI_API_KEY = 'OPENAI_API_KEY',
   OPENAI_MAX_TOKENS = 'OPENAI_MAX_TOKENS',
   OPENAI_BASE_PATH = 'OPENAI_BASE_PATH',
+  OPENAI_API_TYPE = 'OPENAI_API_TYPE',
   description = 'description',
   emoji = 'emoji',
   model = 'model',
@@ -102,11 +103,26 @@ export const configValidators = {
     return value;
   },
 
+  [CONFIG_KEYS.OPENAI_API_TYPE](value: any) {
+    validateConfig(
+      CONFIG_KEYS.OPENAI_API_TYPE,
+      typeof value === 'string',
+      'Must be string'
+    );
+    validateConfig(
+      CONFIG_KEYS.OPENAI_API_TYPE,
+      value === 'azure' || value === 'openai' || value === '',
+      `${value} is not supported yet, use 'azure' or 'openai' (default)`
+    );
+    return value;
+  },
+
   [CONFIG_KEYS.model](value: any) {
     validateConfig(
-      CONFIG_KEYS.OPENAI_BASE_PATH,
-      value === 'gpt-3.5-turbo' || value === 'gpt-4',
-      `${value} is not supported yet, use 'gpt-4' or 'gpt-3.5-turbo' (default)`
+      CONFIG_KEYS.model,
+      value === 'gpt-3.5-turbo' || value === 'gpt-4'
+      || ( typeof value === 'string' && value.match(/^[a-zA-Z0-9~\-]{1,63}[a-zA-Z0-9]$/) ),
+      `${value} is not supported yet, use 'gpt-4' or 'gpt-3.5-turbo' (default) or model deployed name.`
     );
     return value;
   }
diff --git a/src/utils/AzureOpenAI.ts b/src/utils/AzureOpenAI.ts
new file mode 100644
index 00000000..88165774
--- /dev/null
+++ b/src/utils/AzureOpenAI.ts
@@ -0,0 +1,77 @@
+/** Azure OpenAI Service API
+ * https://learn.microsoft.com/azure/cognitive-services/openai/
+ * The API version of the Azure OpenAI Service 2023-03-15-preview
+ *
+ * Note: Only make what is necessary for opencommit
+ */
+
+import {
+  BaseAPI,
+} from 'openai/dist/base';
+import {
+  CreateChatCompletionRequest,
+  Configuration,
+} from "openai";
+import { AxiosRequestConfig} from 'axios';
+
+export class AzureOpenAIApi extends BaseAPI {
+  constructor(configuration: Configuration) {
+    super(configuration);
+  }
+
+  /**
+   *
+   * @summary Creates a completion for the chat message
+   * @param {CreateChatCompletionRequest} createChatCompletionRequest
+   * @param {*} [options] Override http request option.
+   * @throws {RequiredError}
+   * @memberof AzureOpenAIApi
+   */
+  public async createChatCompletion(createChatCompletionRequest: CreateChatCompletionRequest, options?: AxiosRequestConfig) {
+    if (!this.configuration) {
+      throw new Error('Required parameter configuration was null or undefined when calling createChatCompletion.');
+    }
+    if (!this.configuration.basePath) {
+      throw new Error('Required parameter basePath was null or undefined when calling createChatCompletion.');
+    }
+    if (!this.configuration.apiKey) {
+      throw new Error('Required parameter apiKey was null or undefined when calling createChatCompletion.');
+    }
+    if (typeof this.configuration.apiKey !== 'string') {
+      throw new Error('Required parameter apiKey was of type string when calling createChatCompletion.');
+    }
+
+    const url = this.configuration.basePath + 'openai/deployments/' + createChatCompletionRequest.model + '/chat/completions';
+    if (!options) options = {};
+    if (!options.headers) options.headers = {};
+    if (!options.params) options.params = {};
+    options.headers = {
+      'Content-Type': 'application/json',
+      "api-key": this.configuration.apiKey,
+      ...options.headers,
+    }
+    options.params = {
+      'api-version': '2023-03-15-preview',
+      ...options.params,
+    }
+
+    // axios DEBUG
+    // this.axios.interceptors.request.use(request => {
+    //   console.log('Starting Request: ', request)
+    //   return request
+    // })
+    // this.axios.interceptors.response.use(response => {
+    //   console.log('Response: ', response)
+    //   return response
+    // })
+
+    // Azure OpenAI APIのREST呼び出し
+    const response = await this.axios.post(url, createChatCompletionRequest, options);
+
+    // console.log(response.data.usage);
+    return response;
+  }
+
+}
+
+

From 539dc7d7fbe2f95a3acedf15678ebf98a1c8ff54 Mon Sep 17 00:00:00 2001
From: Takuya Ono <takuya-o@users.osdn.me>
Date: Sun, 7 May 2023 16:32:54 +0900
Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20fix(AzureOpenAI.ts):=20fix?=
 =?UTF-8?q?=20import=20path=20for=20AxiosRequestConfig=20to=20avoid=20conf?=
 =?UTF-8?q?licts=20with=20openai's=20axios=20dependency?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In AzureOpenAI.ts, the import path for AxiosRequestConfig was changed to avoid conflicts with openai's axios dependency, which was causing lint errors.
---
 src/utils/AzureOpenAI.ts | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/utils/AzureOpenAI.ts b/src/utils/AzureOpenAI.ts
index 88165774..5b8d04eb 100644
--- a/src/utils/AzureOpenAI.ts
+++ b/src/utils/AzureOpenAI.ts
@@ -5,14 +5,12 @@
  * Note: Only make what is necessary for opencommit
  */
 
-import {
-  BaseAPI,
-} from 'openai/dist/base';
 import {
   CreateChatCompletionRequest,
   Configuration,
 } from "openai";
-import { AxiosRequestConfig} from 'axios';
+import { BaseAPI } from 'openai/dist/base';
+import { AxiosRequestConfig} from 'openai/node_modules/axios';
 
 export class AzureOpenAIApi extends BaseAPI {
   constructor(configuration: Configuration) {

From a78d61d40b29e52d95d63097f586835dd9e56793 Mon Sep 17 00:00:00 2001
From: Takuya Ono <takuya-o@users.osdn.me>
Date: Sun, 7 May 2023 18:16:26 +0900
Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=94=A7=20fix(AzureOpenAI.ts):=20impor?=
 =?UTF-8?q?t=20RequiredError=20to=20fix=20error=20handling=20and=20remove?=
 =?UTF-8?q?=20commented=20out=20debug=20code?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The RequiredError class was not being imported from the openai/dist/base module, causing errors to be thrown incorrectly. This has been fixed by importing the RequiredError class. Debug code has been removed and comments have been updated to reflect the changes made.
---
 src/utils/AzureOpenAI.ts | 22 ++++++----------------
 1 file changed, 6 insertions(+), 16 deletions(-)

diff --git a/src/utils/AzureOpenAI.ts b/src/utils/AzureOpenAI.ts
index 5b8d04eb..bb075698 100644
--- a/src/utils/AzureOpenAI.ts
+++ b/src/utils/AzureOpenAI.ts
@@ -9,7 +9,7 @@ import {
   CreateChatCompletionRequest,
   Configuration,
 } from "openai";
-import { BaseAPI } from 'openai/dist/base';
+import { BaseAPI, RequiredError } from 'openai/dist/base';
 import { AxiosRequestConfig} from 'openai/node_modules/axios';
 
 export class AzureOpenAIApi extends BaseAPI {
@@ -27,16 +27,16 @@ export class AzureOpenAIApi extends BaseAPI {
    */
   public async createChatCompletion(createChatCompletionRequest: CreateChatCompletionRequest, options?: AxiosRequestConfig) {
     if (!this.configuration) {
-      throw new Error('Required parameter configuration was null or undefined when calling createChatCompletion.');
+      throw new RequiredError('configuration', 'Required configuration was null or undefined when calling createChatCompletion.');
     }
     if (!this.configuration.basePath) {
-      throw new Error('Required parameter basePath was null or undefined when calling createChatCompletion.');
+      throw new RequiredError('basePath', 'Required configuration basePath was null or undefined when calling createChatCompletion.');
     }
     if (!this.configuration.apiKey) {
-      throw new Error('Required parameter apiKey was null or undefined when calling createChatCompletion.');
+      throw new RequiredError('apiKey', 'Required configuration apiKey was null or undefined when calling createChatCompletion.');
     }
     if (typeof this.configuration.apiKey !== 'string') {
-      throw new Error('Required parameter apiKey was of type string when calling createChatCompletion.');
+        throw new RequiredError('apiKey', 'Required configuration apiKey was not string when calling createChatCompletion');
     }
 
     const url = this.configuration.basePath + 'openai/deployments/' + createChatCompletionRequest.model + '/chat/completions';
@@ -53,17 +53,7 @@ export class AzureOpenAIApi extends BaseAPI {
       ...options.params,
     }
 
-    // axios DEBUG
-    // this.axios.interceptors.request.use(request => {
-    //   console.log('Starting Request: ', request)
-    //   return request
-    // })
-    // this.axios.interceptors.response.use(response => {
-    //   console.log('Response: ', response)
-    //   return response
-    // })
-
-    // Azure OpenAI APIのREST呼び出し
+    // Azure OpenAI APIのREST call
     const response = await this.axios.post(url, createChatCompletionRequest, options);
 
     // console.log(response.data.usage);

From 7613473b23630b00ef1316a94e41827259eb4987 Mon Sep 17 00:00:00 2001
From: Takuya Ono <takuya-o@users.osdn.me>
Date: Sun, 7 May 2023 18:48:47 +0900
Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=94=87=20chore(AzureOpenAI.ts):=20rem?=
 =?UTF-8?q?ove=20console.log=20statement=20and=20translate=20Japanese=20co?=
 =?UTF-8?q?mment?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The commented console.log statement was removed to improve code cleanliness.
---
 src/utils/AzureOpenAI.ts | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/utils/AzureOpenAI.ts b/src/utils/AzureOpenAI.ts
index bb075698..6a5f9573 100644
--- a/src/utils/AzureOpenAI.ts
+++ b/src/utils/AzureOpenAI.ts
@@ -53,10 +53,9 @@ export class AzureOpenAIApi extends BaseAPI {
       ...options.params,
     }
 
-    // Azure OpenAI APIのREST call
+    // Azure OpenAI API REST call
     const response = await this.axios.post(url, createChatCompletionRequest, options);
 
-    // console.log(response.data.usage);
     return response;
   }
 

From 3b8e4361cba211ebac78bbb6172422e7a51d7162 Mon Sep 17 00:00:00 2001
From: Takuya Ono <takuya-o@users.osdn.me>
Date: Wed, 10 May 2023 01:28:46 +0900
Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=94=A5=20refactoring(api.ts,=20AzureO?=
 =?UTF-8?q?penAI.ts):=20Leverage=20openai=20npm=20package=20=F0=9F=90=9B?=
 =?UTF-8?q?=20fix(config.ts):=20API=20Key=20string=20validation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api.ts               | 31 +++++++++++++------
 src/commands/config.ts   |  8 ++---
 src/utils/AzureOpenAI.ts | 64 ----------------------------------------
 3 files changed, 26 insertions(+), 77 deletions(-)
 delete mode 100644 src/utils/AzureOpenAI.ts

diff --git a/src/api.ts b/src/api.ts
index 4854e4fb..b0806149 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -6,9 +6,6 @@ import {
   Configuration as OpenAiApiConfiguration,
   OpenAIApi
 } from 'openai';
-import {
-  AzureOpenAIApi
-} from './utils/AzureOpenAI';
 
 import { CONFIG_MODES, getConfig } from './commands/config';
 
@@ -40,15 +37,31 @@ class OpenAi {
   private openAiApiConfiguration = new OpenAiApiConfiguration({
     apiKey: apiKey
   });
-  private openAI!: OpenAIApi | AzureOpenAIApi;
+  private openAI!: OpenAIApi;
 
   constructor() {
-    if (basePath) {
-      this.openAiApiConfiguration.basePath = basePath;
+    switch (apiType) {
+      case 'azure':
+        this.openAiApiConfiguration.baseOptions =  {
+          headers: {
+            "api-key": apiKey,
+          },
+          params: {
+            'api-version': '2023-03-15-preview',
+          }
+        };
+        if (basePath) {
+          this.openAiApiConfiguration.basePath = basePath + 'openai/deployments/' + MODEL;
+        }
+        break;
+      case 'openai':
+      default:
+        if (basePath) {
+          this.openAiApiConfiguration.basePath = basePath;
+        }
+        break;
     }
-    this.openAI = apiType === 'azure'
-    ? new AzureOpenAIApi(this.openAiApiConfiguration)
-    : new OpenAIApi(this.openAiApiConfiguration);
+    this.openAI = new OpenAIApi(this.openAiApiConfiguration);
   }
 
   public generateCommitMessage = async (
diff --git a/src/commands/config.ts b/src/commands/config.ts
index 974ab3d0..b9fa8eb7 100644
--- a/src/commands/config.ts
+++ b/src/commands/config.ts
@@ -43,13 +43,13 @@ export const configValidators = {
     validateConfig(CONFIG_KEYS.OPENAI_API_KEY, value, 'Cannot be empty');
     validateConfig(
       CONFIG_KEYS.OPENAI_API_KEY,
-      value.startsWith('sk-'),
-      'Must start with "sk-"'
+      value.startsWith('sk-') || value.match(/^[a-z0-9]{32}$/),
+    'Must start with "sk-". Or a valid Azure OpenAI API key'
     );
     validateConfig(
       CONFIG_KEYS.OPENAI_API_KEY,
-      value.length === 51,
-      'Must be 51 characters long'
+      value.length === 51 || value.length === 32,
+      'Must be 51 or 32 characters long'
     );
 
     return value;
diff --git a/src/utils/AzureOpenAI.ts b/src/utils/AzureOpenAI.ts
deleted file mode 100644
index 6a5f9573..00000000
--- a/src/utils/AzureOpenAI.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/** Azure OpenAI Service API
- * https://learn.microsoft.com/azure/cognitive-services/openai/
- * The API version of the Azure OpenAI Service 2023-03-15-preview
- *
- * Note: Only make what is necessary for opencommit
- */
-
-import {
-  CreateChatCompletionRequest,
-  Configuration,
-} from "openai";
-import { BaseAPI, RequiredError } from 'openai/dist/base';
-import { AxiosRequestConfig} from 'openai/node_modules/axios';
-
-export class AzureOpenAIApi extends BaseAPI {
-  constructor(configuration: Configuration) {
-    super(configuration);
-  }
-
-  /**
-   *
-   * @summary Creates a completion for the chat message
-   * @param {CreateChatCompletionRequest} createChatCompletionRequest
-   * @param {*} [options] Override http request option.
-   * @throws {RequiredError}
-   * @memberof AzureOpenAIApi
-   */
-  public async createChatCompletion(createChatCompletionRequest: CreateChatCompletionRequest, options?: AxiosRequestConfig) {
-    if (!this.configuration) {
-      throw new RequiredError('configuration', 'Required configuration was null or undefined when calling createChatCompletion.');
-    }
-    if (!this.configuration.basePath) {
-      throw new RequiredError('basePath', 'Required configuration basePath was null or undefined when calling createChatCompletion.');
-    }
-    if (!this.configuration.apiKey) {
-      throw new RequiredError('apiKey', 'Required configuration apiKey was null or undefined when calling createChatCompletion.');
-    }
-    if (typeof this.configuration.apiKey !== 'string') {
-        throw new RequiredError('apiKey', 'Required configuration apiKey was not string when calling createChatCompletion');
-    }
-
-    const url = this.configuration.basePath + 'openai/deployments/' + createChatCompletionRequest.model + '/chat/completions';
-    if (!options) options = {};
-    if (!options.headers) options.headers = {};
-    if (!options.params) options.params = {};
-    options.headers = {
-      'Content-Type': 'application/json',
-      "api-key": this.configuration.apiKey,
-      ...options.headers,
-    }
-    options.params = {
-      'api-version': '2023-03-15-preview',
-      ...options.params,
-    }
-
-    // Azure OpenAI API REST call
-    const response = await this.axios.post(url, createChatCompletionRequest, options);
-
-    return response;
-  }
-
-}
-
-