fix: use live copilot model availability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-22 11:07:43 +01:00
co-authored by Copilot
parent 9799a959d6
commit 6e623b7bd6
16 changed files with 691 additions and 267 deletions
@@ -48,10 +48,19 @@ public sealed class SidecarModeCapabilityDto
public string? Reason { get; init; }
}
public sealed class SidecarModelCapabilityDto
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public IReadOnlyList<string> SupportedReasoningEfforts { get; init; } = [];
public string? DefaultReasoningEffort { get; init; }
}
public sealed class SidecarCapabilitiesDto
{
public string Runtime { get; init; } = "dotnet-maf";
public Dictionary<string, SidecarModeCapabilityDto> Modes { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyList<SidecarModelCapabilityDto> Models { get; init; } = [];
}
public class SidecarCommandEnvelope
@@ -1,12 +1,14 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using GitHub.Copilot.SDK;
using Kopaya.AgentHost.Contracts;
namespace Kopaya.AgentHost.Services;
public sealed class SidecarProtocolHost
{
private readonly Func<CancellationToken, Task<SidecarCapabilitiesDto>> _capabilitiesProvider;
private readonly PatternValidator _patternValidator;
private readonly ITurnWorkflowRunner _workflowRunner;
private readonly JsonSerializerOptions _jsonOptions;
@@ -18,10 +20,14 @@ public sealed class SidecarProtocolHost
{
}
public SidecarProtocolHost(PatternValidator patternValidator, ITurnWorkflowRunner? workflowRunner = null)
public SidecarProtocolHost(
PatternValidator patternValidator,
ITurnWorkflowRunner? workflowRunner = null,
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null)
{
_patternValidator = patternValidator;
_workflowRunner = workflowRunner ?? new CopilotWorkflowRunner(_patternValidator);
_capabilitiesProvider = capabilitiesProvider ?? BuildCapabilitiesAsync;
_jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -82,7 +88,7 @@ public sealed class SidecarProtocolHost
{
Type = "capabilities",
RequestId = envelope.RequestId,
Capabilities = BuildCapabilities(),
Capabilities = await _capabilitiesProvider(cancellationToken).ConfigureAwait(false),
}, cancellationToken).ConfigureAwait(false);
break;
@@ -155,8 +161,19 @@ public sealed class SidecarProtocolHost
}
}
private static SidecarCapabilitiesDto BuildCapabilities()
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
{
IReadOnlyList<SidecarModelCapabilityDto> models = [];
try
{
models = await ListAvailableModelsAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
Console.Error.WriteLine($"[kopaya sidecar] Failed to list available Copilot models: {exception.Message}");
}
return new SidecarCapabilitiesDto
{
Modes = new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
@@ -172,6 +189,38 @@ public sealed class SidecarProtocolHost
Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
},
},
Models = models,
};
}
private static async Task<IReadOnlyList<SidecarModelCapabilityDto>> ListAvailableModelsAsync(
CancellationToken cancellationToken)
{
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions();
await using CopilotClient client = new(clientOptions);
await client.StartAsync(cancellationToken).ConfigureAwait(false);
List<ModelInfo> models = await client.ListModelsAsync(cancellationToken).ConfigureAwait(false);
return models
.Select(model => new SidecarModelCapabilityDto
{
Id = model.Id,
Name = model.Name,
SupportedReasoningEfforts = (model.SupportedReasoningEfforts ?? [])
.Where(IsReasoningEffort)
.Distinct(StringComparer.Ordinal)
.ToList(),
DefaultReasoningEffort = IsReasoningEffort(model.DefaultReasoningEffort)
? model.DefaultReasoningEffort
: null,
})
.OrderBy(model => model.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static bool IsReasoningEffort(string? value)
{
return value is "low" or "medium" or "high" or "xhigh";
}
}
@@ -18,7 +18,7 @@ public sealed class SidecarProtocolHostTests
{
Type = "describe-capabilities",
RequestId = "cap-1",
});
}, CreateHostForTests());
Assert.Collection(
events,
@@ -33,6 +33,10 @@ public sealed class SidecarProtocolHostTests
JsonElement modes = capabilities.GetProperty("modes");
Assert.True(modes.GetProperty("single").GetProperty("available").GetBoolean());
Assert.False(modes.GetProperty("magentic").GetProperty("available").GetBoolean());
JsonElement[] models = capabilities.GetProperty("models").EnumerateArray().ToArray();
JsonElement model = Assert.Single(models);
Assert.Equal("gpt-5.4", model.GetProperty("id").GetString());
Assert.Equal("medium", model.GetProperty("defaultReasoningEffort").GetString());
string magenticReason = modes.GetProperty("magentic").GetProperty("reason").GetString() ?? string.Empty;
Assert.Contains("unsupported", magenticReason, StringComparison.OrdinalIgnoreCase);
@@ -220,10 +224,42 @@ public sealed class SidecarProtocolHostTests
using StringReader reader = new(input);
using StringWriter writer = new();
await (host ?? new SidecarProtocolHost()).RunAsync(reader, writer, CancellationToken.None);
await (host ?? CreateHostForTests()).RunAsync(reader, writer, CancellationToken.None);
return ParseEvents(writer.ToString());
}
private static SidecarProtocolHost CreateHostForTests()
{
return new SidecarProtocolHost(
new PatternValidator(),
capabilitiesProvider: _ => Task.FromResult(new SidecarCapabilitiesDto
{
Modes = new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
{
["single"] = new() { Available = true },
["sequential"] = new() { Available = true },
["concurrent"] = new() { Available = true },
["handoff"] = new() { Available = true },
["group-chat"] = new() { Available = true },
["magentic"] = new()
{
Available = false,
Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
},
},
Models =
[
new SidecarModelCapabilityDto
{
Id = "gpt-5.4",
Name = "GPT-5.4",
SupportedReasoningEfforts = ["low", "medium", "high", "xhigh"],
DefaultReasoningEffort = "medium",
},
],
}));
}
private static IReadOnlyList<JsonElement> ParseEvents(string output)
{
List<JsonElement> events = [];