From ea83be98fe2022c2ece63feba4f0b8b0e3af57f1 Mon Sep 17 00:00:00 2001 From: Patrick Ruddiman <86851465+PatrickRuddiman@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:01:10 -0400 Subject: [PATCH 1/2] Clarify Azure support --- Models/AppConfiguration.cs | 3 ++ Program.cs | 7 +++-- README.md | 12 ++++---- Services/ConfigurationService.cs | 53 ++++++++++++++++++++++++++------ Services/OpenAIService.cs | 29 ++++++++++------- WriteCommit.csproj | 1 + 6 files changed, 76 insertions(+), 29 deletions(-) diff --git a/Models/AppConfiguration.cs b/Models/AppConfiguration.cs index b1b853f..a0852f6 100644 --- a/Models/AppConfiguration.cs +++ b/Models/AppConfiguration.cs @@ -18,4 +18,7 @@ public class AppConfiguration [JsonPropertyName("default_topp")] public int? DefaultTopP { get; set; } + + [JsonPropertyName("use_azure_openai")] + public bool UseAzureOpenAI { get; set; } } diff --git a/Program.cs b/Program.cs index 4b09e63..7132734 100644 --- a/Program.cs +++ b/Program.cs @@ -33,10 +33,10 @@ static async Task Main(string[] args) var setupOption = new Option( "--setup", - "Configure OpenAI API key" + "Configure OpenAI or Azure OpenAI settings" ); - var rootCommand = new RootCommand("Generate AI-powered commit messages using OpenAI") + var rootCommand = new RootCommand("Generate AI-powered commit messages using OpenAI or Azure OpenAI") { dryRunOption, verboseOption, @@ -129,7 +129,8 @@ static async Task GenerateCommitMessage( var endpoint = await configService.GetOpenAiEndpointAsync() ?? "https://api.openai.com/v1"; var defaultModel = await configService.GetDefaultModelAsync() ?? "gpt-4o-mini"; - var openAiService = new OpenAIService(apiKey, endpoint); + var useAzure = await configService.UseAzureOpenAIAsync(); + var openAiService = new OpenAIService(apiKey, endpoint, useAzure); // Check if we're in a git repository if (!Directory.Exists(".git") && !await gitService.IsInGitRepositoryAsync()) diff --git a/README.md b/README.md index e0c03b6..5017ad7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WriteCommit -A cross-platform .NET tool that generates AI-powered commit messages using OpenAI's GPT models. +A cross-platform .NET tool that generates AI-powered commit messages using OpenAI or Azure OpenAI. ## โœจ Features @@ -9,7 +9,7 @@ A cross-platform .NET tool that generates AI-powered commit messages using OpenA - ๐ŸŽ›๏ธ **Highly configurable** - Adjust AI parameters to your preference - ๐Ÿงช **Dry-run mode** - Preview generated messages without committing - ๐Ÿ“ **Verbose output** - Detailed logging for debugging and transparency -- โšก **Fast and lightweight** - Direct OpenAI API integration for quick responses +- โšก **Fast and lightweight** - Direct OpenAI or Azure OpenAI integration for quick responses - ๐Ÿ“‹ **Smart chunking** - Handles large diffs by intelligently splitting them into semantic chunks - ๐Ÿ” **Context-aware** - Adds surrounding code lines when diffs are very small for better summaries @@ -18,7 +18,7 @@ A cross-platform .NET tool that generates AI-powered commit messages using OpenA ### Prerequisites - [.NET 8.0 or later](https://dotnet.microsoft.com/download) - - OpenAI API key (optional, required only if your endpoint needs authentication) +- OpenAI or Azure OpenAI API key (optional, required only if your endpoint needs authentication) - Git repository with staged changes ### Installation @@ -101,13 +101,13 @@ WriteCommit --dry-run --verbose --temperature 0.5 | `--model` | from setup | OpenAI model to use | | `--presence` | `0` | Presence penalty (-2 to 2) | | `--frequency` | `0` | Frequency penalty (-2 to 2) | -| `--setup` | `false` | Configure OpenAI settings | +| `--setup` | `false` | Configure OpenAI or Azure OpenAI settings | ## ๐Ÿ”ง How It Works 1. **Validates environment** - Checks for git repository and OpenAI settings 2. **Analyzes changes** - Processes your staged git diff using semantic chunking -3. **Generates message** - Uses OpenAI API to create meaningful commit message +3. **Generates message** - Uses OpenAI or Azure OpenAI to create meaningful commit message 4. **Commits changes** - Applies the generated message (unless `--dry-run`) ## ๐Ÿ”‘ Configuration @@ -121,7 +121,7 @@ WriteCommit --dry-run --verbose --temperature 0.5 WriteCommit --setup ``` -This will prompt you to enter your API key (if needed), API endpoint, and default model, then save them to `~/.writecommit/config.json`. +This will prompt you to enter your API key (if needed), choose between OpenAI or Azure OpenAI, specify the endpoint, and default model/deployment. The values are saved to `~/.writecommit/config.json`. **Option 2: Using Environment Variables** diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 393ea74..48a6853 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -1,5 +1,7 @@ using System.Text.Json; using OpenAI.Chat; +using Azure.AI.OpenAI; +using System.ClientModel; using WriteCommit.Constants; using WriteCommit.Models; @@ -94,6 +96,15 @@ public async Task SaveConfigurationAsync(AppConfiguration config) return config?.DefaultModel; } + /// + /// Returns true if configuration specifies Azure OpenAI usage + /// + public async Task UseAzureOpenAIAsync() + { + var config = await LoadConfigurationAsync(); + return config?.UseAzureOpenAI ?? false; + } + /// /// Prompts user to enter and save their OpenAI API key /// @@ -102,8 +113,8 @@ public async Task SetupApiKeyAsync(bool verbose = false) Console.WriteLine("WriteCommit Setup"); Console.WriteLine("================="); Console.WriteLine(); - Console.WriteLine("Please enter your OpenAI API key."); - Console.WriteLine("You can get one from: https://platform.openai.com/api-keys"); + Console.WriteLine("Please enter your OpenAI API key (or Azure OpenAI key)."); + Console.WriteLine("You can get one from: https://platform.openai.com/api-keys or your Azure portal"); Console.WriteLine(); Console.Write("API Key (leave blank if not required): "); @@ -116,14 +127,27 @@ public async Task SetupApiKeyAsync(bool verbose = false) apiKey = null; } - // Prompt for endpoint and model - Console.Write($"Endpoint (default: https://api.openai.com/v1): "); + // Ask if using Azure OpenAI + Console.Write("Use Azure OpenAI service? (y/N): "); + var azureInput = Console.ReadLine()?.Trim().ToLowerInvariant(); + bool useAzure = azureInput == "y" || azureInput == "yes"; + + // Prompt for endpoint and model/deployment + Console.Write( + useAzure + ? "Azure endpoint (e.g. https://your-resource.openai.azure.com): " + : "Endpoint (default: https://api.openai.com/v1): " + ); var endpointInput = Console.ReadLine()?.Trim(); var endpoint = string.IsNullOrWhiteSpace(endpointInput) - ? "https://api.openai.com/v1" + ? (useAzure ? "https://your-resource.openai.azure.com" : "https://api.openai.com/v1") : endpointInput; - Console.Write($"Default model (default: gpt-4o-mini): "); + Console.Write( + useAzure + ? "Deployment name (default: gpt-4o-mini): " + : "Default model (default: gpt-4o-mini): " + ); var modelInput = Console.ReadLine()?.Trim(); var model = string.IsNullOrWhiteSpace(modelInput) ? "gpt-4o-mini" : modelInput; @@ -132,6 +156,7 @@ public async Task SetupApiKeyAsync(bool verbose = false) config.OpenAiApiKey = apiKey; config.OpenAiEndpoint = endpoint; config.DefaultModel = model; + config.UseAzureOpenAI = useAzure; // Save configuration await SaveConfigurationAsync(config); @@ -151,7 +176,7 @@ public async Task SetupApiKeyAsync(bool verbose = false) if ((testResponse == "y" || testResponse == "yes") && !string.IsNullOrEmpty(apiKey)) { - return await TestApiKeyAsync(apiKey, verbose); + return await TestApiKeyAsync(apiKey, useAzure, endpoint, model, verbose); } return true; @@ -160,13 +185,23 @@ public async Task SetupApiKeyAsync(bool verbose = false) /// /// Tests if the API key is valid by making a simple request /// - private async Task TestApiKeyAsync(string? apiKey, bool verbose) + private async Task TestApiKeyAsync(string? apiKey, bool useAzure, string endpoint, string model, bool verbose) { Console.WriteLine("Testing API key..."); try { - var testClient = new ChatClient("gpt-4o-mini", apiKey); + ChatClient testClient; + if (useAzure) + { + var azureClient = new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(endpoint), new ApiKeyCredential(apiKey)); + testClient = azureClient.GetChatClient(model); + } + else + { + var options = new OpenAI.OpenAIClientOptions { Endpoint = new Uri(endpoint) }; + testClient = new ChatClient(model, new ApiKeyCredential(apiKey), options); + } var messages = new List { new SystemChatMessage("You are a helpful assistant."), diff --git a/Services/OpenAIService.cs b/Services/OpenAIService.cs index 39c4b56..10b5d2c 100644 --- a/Services/OpenAIService.cs +++ b/Services/OpenAIService.cs @@ -1,6 +1,7 @@ using System.Text; using OpenAI.Chat; using System.ClientModel; +using Azure.AI.OpenAI; using WriteCommit.Constants; using WriteCommit.Models; @@ -10,10 +11,11 @@ public class OpenAIService { private readonly string _apiKey; private readonly string _endpoint; + private readonly bool _useAzure; private readonly string _patternsDirectory; private const int MaxContextTokens = 128000; - public OpenAIService(string apiKey, string? endpoint = null) + public OpenAIService(string apiKey, string? endpoint = null, bool useAzure = false) { if (string.IsNullOrEmpty(apiKey)) { @@ -24,9 +26,22 @@ public OpenAIService(string apiKey, string? endpoint = null) _endpoint = string.IsNullOrWhiteSpace(endpoint) ? "https://api.openai.com/v1" : endpoint; + _useAzure = useAzure; _patternsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "patterns"); } + private ChatClient CreateChatClient(string model) + { + if (_useAzure) + { + var azureClient = new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(_endpoint), new ApiKeyCredential(_apiKey)); + return azureClient.GetChatClient(model); + } + + var options = new OpenAI.OpenAIClientOptions { Endpoint = new Uri(_endpoint) }; + return new ChatClient(model, new ApiKeyCredential(_apiKey), options); + } + public async Task GenerateCommitMessageAsync( List chunks, string pattern, @@ -155,11 +170,7 @@ bool verbose } // Create a client for this specific model - var clientOptions = new OpenAI.OpenAIClientOptions - { - Endpoint = new Uri(_endpoint) - }; - var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions); + var chatClient = CreateChatClient(model); // Create the chat messages var messages = new List @@ -276,11 +287,7 @@ bool verbose } // Create a client for this specific model - var clientOptions = new OpenAI.OpenAIClientOptions - { - Endpoint = new Uri(_endpoint) - }; - var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions); + var chatClient = CreateChatClient(model); // Create the chat messages var messages = new List diff --git a/WriteCommit.csproj b/WriteCommit.csproj index b714c70..1177262 100644 --- a/WriteCommit.csproj +++ b/WriteCommit.csproj @@ -23,6 +23,7 @@ true + From 1e41dc1569fee0ab6aa04b202b70bb4cd37ba94d Mon Sep 17 00:00:00 2001 From: Patrick Ruddiman <86851465+PatrickRuddiman@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:56:48 -0400 Subject: [PATCH 2/2] Handle blank Azure endpoint --- README.md | 2 +- Services/ConfigurationService.cs | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5017ad7..21ec9f8 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ WriteCommit --dry-run --verbose --temperature 0.5 WriteCommit --setup ``` -This will prompt you to enter your API key (if needed), choose between OpenAI or Azure OpenAI, specify the endpoint, and default model/deployment. The values are saved to `~/.writecommit/config.json`. +This will prompt you to enter your API key (if needed), choose between OpenAI or Azure OpenAI, specify the endpoint, and default model/deployment. If you select Azure but leave the endpoint blank, WriteCommit will fall back to the standard OpenAI endpoint. The values are saved to `~/.writecommit/config.json`. **Option 2: Using Environment Variables** diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 48a6853..53f263b 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -135,13 +135,24 @@ public async Task SetupApiKeyAsync(bool verbose = false) // Prompt for endpoint and model/deployment Console.Write( useAzure - ? "Azure endpoint (e.g. https://your-resource.openai.azure.com): " + ? "Azure endpoint (leave blank to use OpenAI): " : "Endpoint (default: https://api.openai.com/v1): " ); var endpointInput = Console.ReadLine()?.Trim(); - var endpoint = string.IsNullOrWhiteSpace(endpointInput) - ? (useAzure ? "https://your-resource.openai.azure.com" : "https://api.openai.com/v1") - : endpointInput; + string endpoint; + if (string.IsNullOrWhiteSpace(endpointInput)) + { + endpoint = "https://api.openai.com/v1"; + // If no Azure endpoint provided, fall back to OpenAI + if (useAzure) + { + useAzure = false; + } + } + else + { + endpoint = endpointInput; + } Console.Write( useAzure