diff --git a/Models/AppConfiguration.cs b/Models/AppConfiguration.cs index 58a195a..b1b853f 100644 --- a/Models/AppConfiguration.cs +++ b/Models/AppConfiguration.cs @@ -10,6 +10,9 @@ public class AppConfiguration [JsonPropertyName("default_model")] public string? DefaultModel { get; set; } + [JsonPropertyName("openai_endpoint")] + public string? OpenAiEndpoint { get; set; } + [JsonPropertyName("default_temperature")] public int? DefaultTemperature { get; set; } diff --git a/Program.cs b/Program.cs index d020c65..14dbd69 100644 --- a/Program.cs +++ b/Program.cs @@ -14,11 +14,6 @@ static async Task Main(string[] args) "Generate commit message without committing" ); var verboseOption = new Option("--verbose", "Show detailed output"); - var patternOption = new Option( - "--pattern", - () => PatternNames.CommitPattern, - "Pattern to use for message generation" - ); var temperatureOption = new Option( "--temperature", () => 1, @@ -31,10 +26,9 @@ static async Task Main(string[] args) () => 0, "Frequency penalty for AI model" ); - var modelOption = new Option( + var modelOption = new Option( "--model", - () => "gpt-4o-mini", - "AI model to use (default: gpt-4o-mini)" + description: "AI model to use (overrides setup)" ); var setupOption = new Option( @@ -46,7 +40,6 @@ static async Task Main(string[] args) { dryRunOption, verboseOption, - patternOption, temperatureOption, topPOption, presenceOption, @@ -59,12 +52,11 @@ static async Task Main(string[] args) async ( bool dryRun, bool verbose, - string pattern, int temperature, int topP, int presence, int frequency, - string model + string? model ) => { try @@ -84,7 +76,6 @@ string model await GenerateCommitMessage( dryRun, verbose, - pattern, temperature, topP, presence, @@ -100,7 +91,6 @@ await GenerateCommitMessage( }, dryRunOption, verboseOption, - patternOption, temperatureOption, topPOption, presenceOption, @@ -114,12 +104,11 @@ await GenerateCommitMessage( static async Task GenerateCommitMessage( bool dryRun, bool verbose, - string pattern, int temperature, int topP, int presence, int frequency, - string model + string? model ) { var gitService = new GitService(); @@ -137,7 +126,10 @@ string model } // Create OpenAI service with the API key - var openAiService = new OpenAIService(apiKey); + var endpoint = await configService.GetOpenAiEndpointAsync() ?? "https://api.openai.com/v1"; + var defaultModel = await configService.GetDefaultModelAsync() ?? "gpt-4o-mini"; + + var openAiService = new OpenAIService(apiKey, endpoint); // Check if we're in a git repository if (!Directory.Exists(".git") && !await gitService.IsInGitRepositoryAsync()) @@ -182,14 +174,15 @@ string model } // Generate commit message using OpenAI with chunking support + var finalModel = string.IsNullOrWhiteSpace(model) ? defaultModel : model; var commitMessage = await openAiService.GenerateCommitMessageAsync( chunks, - pattern, + PatternNames.CommitPattern, temperature, topP, presence, frequency, - model, + finalModel, verbose ); diff --git a/README.md b/README.md index fa1140b..275d6b1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A cross-platform .NET tool that generates AI-powered commit messages using OpenA - ๐Ÿค– **AI-powered commit messages** - Generate meaningful commit messages from your staged changes - ๐Ÿ”„ **Cross-platform** - Works on Windows, macOS, and Linux -- ๐ŸŽ›๏ธ **Highly configurable** - Adjust AI parameters and patterns to your preference +- ๐ŸŽ›๏ธ **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 @@ -17,7 +17,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 (either set as `OPENAI_API_KEY` environment variable or via `--setup`) + - OpenAI API key (optional, required only if your endpoint needs authentication) - Git repository with staged changes ### Installation @@ -83,13 +83,10 @@ WriteCommit --dry-run WriteCommit --verbose # Custom AI parameters -WriteCommit --temperature 0.7 --topp 0.9 --pattern custom_pattern - -# Force reinstall all patterns -WriteCommit --reinstall-patterns +WriteCommit --temperature 0.7 --topp 0.9 # Combine multiple options -WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns +WriteCommit --dry-run --verbose --temperature 0.5 ``` ## โš™๏ธ Configuration Options @@ -98,18 +95,16 @@ WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns |--------|---------|-------------| | `--dry-run` | `false` | Generate message without committing | | `--verbose` | `false` | Show detailed output | -| `--pattern` | `write_commit_message` | Pattern to use for message generation | | `--temperature` | `1` | AI creativity level (0-2) | | `--topp` | `1` | Nucleus sampling parameter (0-1) | -| `--model` | `gpt-4o-mini` | OpenAI model to use | +| `--model` | from setup | OpenAI model to use | | `--presence` | `0` | Presence penalty (-2 to 2) | | `--frequency` | `0` | Frequency penalty (-2 to 2) | -| `--reinstall-patterns` | `false` | Force reinstallation of all patterns | -| `--setup` | `false` | Configure OpenAI API key | +| `--setup` | `false` | Configure OpenAI settings | ## ๐Ÿ”ง How It Works -1. **Validates environment** - Checks for git repository and OpenAI API key +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 4. **Commits changes** - Applies the generated message (unless `--dry-run`) @@ -125,19 +120,19 @@ WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns WriteCommit --setup ``` -This will prompt you to enter your API key and securely save it to `~/.writecommit/config.json`. +This will prompt you to enter your API key (if needed), API endpoint, and default model, then save them to `~/.writecommit/config.json`. **Option 2: Using Environment Variables** ```bash # Linux/macOS -export OPENAI_API_KEY="your-api-key-here" +export OPENAI_API_KEY="your-api-key-here" # optional # Windows (PowerShell) -$env:OPENAI_API_KEY="your-api-key-here" +$env:OPENAI_API_KEY="your-api-key-here" # optional # Windows (Command Prompt) -set OPENAI_API_KEY=your-api-key-here +set OPENAI_API_KEY=your-api-key-here # optional ``` For persistent configuration, add the export to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.) or Windows environment variables. diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 459020c..393ea74 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -76,6 +76,24 @@ public async Task SaveConfigurationAsync(AppConfiguration config) return config?.OpenAiApiKey; } + /// + /// Gets the configured OpenAI endpoint or null if not set + /// + public async Task GetOpenAiEndpointAsync() + { + var config = await LoadConfigurationAsync(); + return config?.OpenAiEndpoint; + } + + /// + /// Gets the configured default model or null if not set + /// + public async Task GetDefaultModelAsync() + { + var config = await LoadConfigurationAsync(); + return config?.DefaultModel; + } + /// /// Prompts user to enter and save their OpenAI API key /// @@ -87,7 +105,7 @@ public async Task SetupApiKeyAsync(bool verbose = false) Console.WriteLine("Please enter your OpenAI API key."); Console.WriteLine("You can get one from: https://platform.openai.com/api-keys"); Console.WriteLine(); - Console.Write("API Key: "); + Console.Write("API Key (leave blank if not required): "); // Read API key with masked input var apiKey = ReadMaskedInput(); @@ -95,25 +113,30 @@ public async Task SetupApiKeyAsync(bool verbose = false) if (string.IsNullOrWhiteSpace(apiKey)) { - Console.WriteLine("No API key entered. Setup cancelled."); - return false; + apiKey = null; } - // Basic validation - if (!apiKey.StartsWith("sk-")) - { - Console.WriteLine("Invalid API key format. OpenAI API keys should start with 'sk-'."); - return false; - } + // Prompt for endpoint and model + Console.Write($"Endpoint (default: https://api.openai.com/v1): "); + var endpointInput = Console.ReadLine()?.Trim(); + var endpoint = string.IsNullOrWhiteSpace(endpointInput) + ? "https://api.openai.com/v1" + : endpointInput; + + Console.Write($"Default model (default: gpt-4o-mini): "); + var modelInput = Console.ReadLine()?.Trim(); + var model = string.IsNullOrWhiteSpace(modelInput) ? "gpt-4o-mini" : modelInput; // Load existing config or create new one var config = await LoadConfigurationAsync() ?? new AppConfiguration(); config.OpenAiApiKey = apiKey; + config.OpenAiEndpoint = endpoint; + config.DefaultModel = model; // Save configuration await SaveConfigurationAsync(config); - Console.WriteLine($"โœ… API key saved to {_configFilePath}"); + Console.WriteLine($"โœ… Configuration saved to {_configFilePath}"); if (verbose) { @@ -126,7 +149,7 @@ public async Task SetupApiKeyAsync(bool verbose = false) Console.Write("Would you like to test the API key? (y/N): "); var testResponse = Console.ReadLine()?.Trim().ToLowerInvariant(); - if (testResponse == "y" || testResponse == "yes") + if ((testResponse == "y" || testResponse == "yes") && !string.IsNullOrEmpty(apiKey)) { return await TestApiKeyAsync(apiKey, verbose); } @@ -137,7 +160,7 @@ 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 verbose) { Console.WriteLine("Testing API key..."); diff --git a/Services/OpenAIService.cs b/Services/OpenAIService.cs index 5064643..39c4b56 100644 --- a/Services/OpenAIService.cs +++ b/Services/OpenAIService.cs @@ -1,5 +1,6 @@ using System.Text; using OpenAI.Chat; +using System.ClientModel; using WriteCommit.Constants; using WriteCommit.Models; @@ -8,10 +9,11 @@ namespace WriteCommit.Services; public class OpenAIService { private readonly string _apiKey; + private readonly string _endpoint; private readonly string _patternsDirectory; private const int MaxContextTokens = 128000; - public OpenAIService(string apiKey) + public OpenAIService(string apiKey, string? endpoint = null) { if (string.IsNullOrEmpty(apiKey)) { @@ -19,6 +21,9 @@ public OpenAIService(string apiKey) } _apiKey = apiKey; + _endpoint = string.IsNullOrWhiteSpace(endpoint) + ? "https://api.openai.com/v1" + : endpoint; _patternsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "patterns"); } @@ -150,7 +155,11 @@ bool verbose } // Create a client for this specific model - var chatClient = new ChatClient(model, _apiKey); + var clientOptions = new OpenAI.OpenAIClientOptions + { + Endpoint = new Uri(_endpoint) + }; + var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions); // Create the chat messages var messages = new List @@ -267,7 +276,11 @@ bool verbose } // Create a client for this specific model - var chatClient = new ChatClient(model, _apiKey); + var clientOptions = new OpenAI.OpenAIClientOptions + { + Endpoint = new Uri(_endpoint) + }; + var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions); // Create the chat messages var messages = new List