Skip to content

Commit 4bd2161

Browse files
committed
feat: Add model configuration management (#55)
Implemented comprehensive model configuration system following the same pattern as image configuration. Features: - Model listing via error parsing (--list-models) - Set/get/clear model config (local & global) - Visual indicator on startup showing active model - Support for 'default' keyword to use Copilot CLI default - Configuration priority: CLI > Local > Global > Default Commands: - --list-models - Lists available models from Copilot CLI - --show-model - Shows current model configuration - --set-model <id> / --set-model-global <id> - Save model - --clear-model / --clear-model-global - Clear config Visual indicators: - 🤖 Using model: gpt-5 📍 (local) - 🤖 Using model: claude-sonnet-4.5 🌍 (global) - 🤖 Using model: gpt-5-mini 🔧 (CLI) - 🤖 Using model: default (none set) Infrastructure: - Added DockerRunner.RunAndCapture() for output capture - Regex parser for Copilot CLI error messages - 294 unit tests (all passing) Files modified: 12 (including image set messages) Files created: 10 (Model commands + tests + docs) Version: 2026.01.05 Co-authored-by: Gordon Beeming <[email protected]>
1 parent 189e701 commit 4bd2161

23 files changed

+893
-11
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ grep "BuildDate = " app/Infrastructure/BuildInfo.cs
7979
- Ensure both scripts are kept in sync regarding functionality and version numbers.
8080
- Both scripts MUST have identical version numbers at all times.
8181

82-
**Current version**: 2026.01.04
82+
**Current version**: 2026.01.05
8383

8484
## Technology Stack
8585

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ obj/
1212
# --- IDE & Editor Settings ---
1313
.vs/
1414
.vscode/
15+
.idea/
1516
*.user
1617
*.suo
1718
*.diff

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
33
<!-- Central version for all projects - use date format YYYY.MM.DD or YYYY.MM.DD.N for same-day updates -->
4-
<CopilotHereVersion>2026.01.04</CopilotHereVersion>
4+
<CopilotHereVersion>2026.01.05</CopilotHereVersion>
55
</PropertyGroup>
66
</Project>

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,47 @@ You can configure the default image tag to use (e.g., `dotnet`, `dotnet-playwrig
105105
- Global: `~/.config/copilot_here/image.conf`
106106
- Local: `.copilot_here/image.conf`
107107

108+
### Model Management
109+
110+
You can configure the default AI model to use so you don't have to pass `--model` every time.
111+
112+
**Management Commands:**
113+
- `--list-models` - List available AI models (queries Copilot CLI error message)
114+
- `--show-model` - Show current default model configuration
115+
- `--set-model <model-id>` - Set default model in local config
116+
- `--set-model-global <model-id>` - Set default model in global config
117+
- `--clear-model` - Clear default model from local config
118+
- `--clear-model-global` - Clear default model from global config
119+
120+
**Special value:** Use `default` as the model ID to explicitly use Copilot CLI's default model. This is useful for overriding a global setting at the local level.
121+
122+
**Note:** The `--list-models` command uses a workaround by triggering an invalid model error, which causes the Copilot CLI to list valid models. This is a temporary approach until a proper API is available.
123+
124+
**Configuration Files:**
125+
- Global: `~/.config/copilot_here/model.conf`
126+
- Local: `.copilot_here/model.conf`
127+
128+
**Configuration Priority:**
129+
1. CLI argument (`--model <model-id>`)
130+
2. Local config (`.copilot_here/model.conf`)
131+
3. Global config (`~/.config/copilot_here/model.conf`)
132+
4. Default (GitHub Copilot CLI default)
133+
134+
**Example Usage:**
135+
```bash
136+
# Set model for current project
137+
copilot_here --set-model gpt-5
138+
139+
# Set model globally for all projects
140+
copilot_here --set-model-global claude-sonnet-4.5
141+
142+
# Override saved model for one session
143+
copilot_here --model gpt-5-mini
144+
145+
# View current configuration
146+
copilot_here --show-model
147+
```
148+
108149
### Custom Docker Flags (SANDBOX_FLAGS)
109150

110151
Pass additional Docker flags using the `SANDBOX_FLAGS` environment variable (compatible with Gemini CLI):

app/Commands/Images/SetImage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ private static Command SetSetImageCommand()
1717
var tag = parseResult.GetValue(tagArg)!;
1818
var paths = AppPaths.Resolve();
1919
ImageConfig.SaveLocal(paths, tag);
20-
Console.WriteLine($"✅ Set local default image: {tag}");
20+
Console.WriteLine($"✅ Set local image: {tag}");
2121
return 0;
2222
});
2323
return command;

app/Commands/Images/SetImageGlobal.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ private static Command SetSetImageGlobalCommand()
1717
var tag = parseResult.GetValue(tagArg)!;
1818
var paths = AppPaths.Resolve();
1919
ImageConfig.SaveGlobal(paths, tag);
20-
Console.WriteLine($"✅ Set global default image: {tag}");
20+
Console.WriteLine($"✅ Set global image: {tag}");
2121
return 0;
2222
});
2323
return command;

app/Commands/Model/ClearModel.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.CommandLine;
2+
using CopilotHere.Infrastructure;
3+
4+
namespace CopilotHere.Commands.Model;
5+
6+
public sealed partial class ModelCommands
7+
{
8+
private static Command SetClearModelCommand()
9+
{
10+
var command = new Command("--clear-model", "Clear local model configuration");
11+
command.SetAction(_ =>
12+
{
13+
var paths = AppPaths.Resolve();
14+
var deleted = ModelConfig.ClearLocal(paths);
15+
if (deleted)
16+
Console.WriteLine("✅ Cleared local model configuration");
17+
else
18+
Console.WriteLine("ℹ️ No local model configuration to clear");
19+
return 0;
20+
});
21+
return command;
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.CommandLine;
2+
using CopilotHere.Infrastructure;
3+
4+
namespace CopilotHere.Commands.Model;
5+
6+
public sealed partial class ModelCommands
7+
{
8+
private static Command SetClearModelGlobalCommand()
9+
{
10+
var command = new Command("--clear-model-global", "Clear global model configuration");
11+
command.SetAction(_ =>
12+
{
13+
var paths = AppPaths.Resolve();
14+
var deleted = ModelConfig.ClearGlobal(paths);
15+
if (deleted)
16+
Console.WriteLine("✅ Cleared global model configuration");
17+
else
18+
Console.WriteLine("ℹ️ No global model configuration to clear");
19+
return 0;
20+
});
21+
return command;
22+
}
23+
}

app/Commands/Model/ListModels.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
}

app/Commands/Model/SetModel.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.CommandLine;
2+
using CopilotHere.Infrastructure;
3+
4+
namespace CopilotHere.Commands.Model;
5+
6+
public sealed partial class ModelCommands
7+
{
8+
private static Command SetSetModelCommand()
9+
{
10+
var command = new Command("--set-model", "Set default model in local config");
11+
12+
var modelArg = new Argument<string>("model") { Description = "Model ID to set" };
13+
command.Add(modelArg);
14+
15+
command.SetAction(parseResult =>
16+
{
17+
var model = parseResult.GetValue(modelArg)!;
18+
var paths = AppPaths.Resolve();
19+
ModelConfig.SaveLocal(paths, model);
20+
Console.WriteLine($"✅ Set local model: {model}");
21+
return 0;
22+
});
23+
return command;
24+
}
25+
}

0 commit comments

Comments
 (0)