|
| 1 | +using System.CommandLine; |
| 2 | +using System.Text.RegularExpressions; |
| 3 | +using CopilotHere.Infrastructure; |
| 4 | +using AppContext = CopilotHere.Infrastructure.AppContext; |
| 5 | + |
| 6 | +namespace CopilotHere.Commands.Model; |
| 7 | + |
| 8 | +public sealed partial class ModelCommands |
| 9 | +{ |
| 10 | + private static Command SetListModelsCommand() |
| 11 | + { |
| 12 | + var command = new Command("--list-models", "List available AI models from GitHub Copilot CLI"); |
| 13 | + command.SetAction(_ => |
| 14 | + { |
| 15 | + Console.WriteLine("🤖 Fetching available models..."); |
| 16 | + Console.WriteLine(); |
| 17 | + |
| 18 | + var ctx = AppContext.Create(); |
| 19 | + var imageTag = ctx.ImageConfig.Tag; |
| 20 | + var imageName = DockerRunner.GetImageName(imageTag); |
| 21 | + |
| 22 | + // Pull image if needed (quietly) |
| 23 | + if (!DockerRunner.PullImage(imageName)) |
| 24 | + { |
| 25 | + Console.WriteLine("❌ Failed to pull Docker image"); |
| 26 | + return 1; |
| 27 | + } |
| 28 | + |
| 29 | + // Run copilot with an invalid model to trigger error that lists valid models |
| 30 | + var args = new List<string> |
| 31 | + { |
| 32 | + "run", |
| 33 | + "--rm", |
| 34 | + "--env", $"GH_TOKEN={ctx.Environment.GitHubToken}", |
| 35 | + imageName, |
| 36 | + "copilot", // Just "copilot", not "gh copilot" |
| 37 | + "--model", "invalid-model-to-trigger-list" |
| 38 | + }; |
| 39 | + |
| 40 | + DebugLogger.Log("Running: docker run ... copilot --model invalid-model-to-trigger-list"); |
| 41 | + var (exitCode, stdout, stderr) = DockerRunner.RunAndCapture(args); |
| 42 | + |
| 43 | + DebugLogger.Log($"Exit code: {exitCode}"); |
| 44 | + DebugLogger.Log($"stderr: {stderr}"); |
| 45 | + DebugLogger.Log($"stdout: {stdout}"); |
| 46 | + |
| 47 | + // Parse the error message to extract model list |
| 48 | + var models = ParseModelListFromError(stderr); |
| 49 | + |
| 50 | + if (models.Count == 0) |
| 51 | + { |
| 52 | + // Fallback: show instructions if parsing fails |
| 53 | + Console.WriteLine("❌ Could not parse model list from Copilot CLI output"); |
| 54 | + Console.WriteLine(); |
| 55 | + Console.WriteLine("To see available models:"); |
| 56 | + Console.WriteLine(" 1. Run: copilot_here"); |
| 57 | + Console.WriteLine(" 2. Type: /model"); |
| 58 | + Console.WriteLine(); |
| 59 | + Console.WriteLine("Raw error output:"); |
| 60 | + Console.WriteLine(stderr); |
| 61 | + return 1; |
| 62 | + } |
| 63 | + |
| 64 | + Console.WriteLine("Available models:"); |
| 65 | + foreach (var model in models) |
| 66 | + { |
| 67 | + Console.WriteLine($" • {model}"); |
| 68 | + } |
| 69 | + Console.WriteLine(); |
| 70 | + Console.WriteLine("💡 Set your preferred model:"); |
| 71 | + Console.WriteLine(" copilot_here --set-model <model-id> (local project)"); |
| 72 | + Console.WriteLine(" copilot_here --set-model-global <model-id> (all projects)"); |
| 73 | + |
| 74 | + return 0; |
| 75 | + }); |
| 76 | + return command; |
| 77 | + } |
| 78 | + |
| 79 | + private static List<string> ParseModelListFromError(string errorOutput) |
| 80 | + { |
| 81 | + var models = new List<string>(); |
| 82 | + |
| 83 | + // Look for "Allowed choices are model1, model2, model3." |
| 84 | + // Need to handle dots in model names (e.g., gpt-5.1) |
| 85 | + // Match everything after "Allowed choices are" until period followed by newline or end |
| 86 | + var match = Regex.Match(errorOutput, @"Allowed choices are\s+(.+?)\.(?:\s|$)", RegexOptions.IgnoreCase | RegexOptions.Singleline); |
| 87 | + if (match.Success) |
| 88 | + { |
| 89 | + var modelString = match.Groups[1].Value; |
| 90 | + DebugLogger.Log($"Captured model string: '{modelString}'"); |
| 91 | + |
| 92 | + // Split by comma |
| 93 | + var parts = modelString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
| 94 | + foreach (var part in parts) |
| 95 | + { |
| 96 | + var cleaned = part.Trim(); |
| 97 | + if (!string.IsNullOrWhiteSpace(cleaned)) |
| 98 | + { |
| 99 | + models.Add(cleaned); |
| 100 | + DebugLogger.Log($"Added model: '{cleaned}'"); |
| 101 | + } |
| 102 | + } |
| 103 | + return models; |
| 104 | + } |
| 105 | + |
| 106 | + DebugLogger.Log("'Allowed choices are' pattern did not match"); |
| 107 | + |
| 108 | + // Fallback: Look for patterns like "valid values are:" or "available models:" |
| 109 | + match = Regex.Match(errorOutput, @"(?:valid|available)[^:]*:\s*(.+)", RegexOptions.IgnoreCase); |
| 110 | + if (match.Success) |
| 111 | + { |
| 112 | + var modelString = match.Groups[1].Value; |
| 113 | + var parts = Regex.Split(modelString, @"[,;\n]"); |
| 114 | + foreach (var part in parts) |
| 115 | + { |
| 116 | + var cleaned = part.Trim().Trim('"', '\'', '`', '.', ' '); |
| 117 | + if (!string.IsNullOrWhiteSpace(cleaned) && |
| 118 | + !cleaned.Contains("and") && |
| 119 | + !cleaned.Contains("or") && |
| 120 | + cleaned.Length < 50) |
| 121 | + { |
| 122 | + models.Add(cleaned); |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + return models.Distinct().ToList(); |
| 128 | + } |
| 129 | +} |
0 commit comments