diff --git a/src/Primitives/CrestApps.Core.AI.Claude/Handlers/ClaudeChatInteractionSettingsHandler.cs b/src/Primitives/CrestApps.Core.AI.Claude/Handlers/ClaudeChatInteractionSettingsHandler.cs index 3dade02f..bce9cc8f 100644 --- a/src/Primitives/CrestApps.Core.AI.Claude/Handlers/ClaudeChatInteractionSettingsHandler.cs +++ b/src/Primitives/CrestApps.Core.AI.Claude/Handlers/ClaudeChatInteractionSettingsHandler.cs @@ -19,6 +19,7 @@ public Task UpdatingAsync(ChatInteraction interaction, JsonElement settings) interaction.Alter(metadata => { metadata.ClaudeModel = GetString(settings, "anthropicModel"); + metadata.EffortLevel = GetEnum(settings, "anthropicEffortLevel"); }); return Task.CompletedTask; @@ -38,4 +39,22 @@ private static string GetString(JsonElement element, string propertyName) return null; } + + private static T GetEnum(JsonElement element, string propertyName) where T : struct, Enum + { + if (element.TryGetProperty(propertyName, out var prop)) + { + if (prop.ValueKind == JsonValueKind.Number && Enum.IsDefined(typeof(T), prop.GetInt32())) + { + return (T)(object)prop.GetInt32(); + } + + if (prop.ValueKind == JsonValueKind.String && Enum.TryParse(prop.GetString(), ignoreCase: true, out var result)) + { + return result; + } + } + + return default; + } } diff --git a/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeEffortLevel.cs b/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeEffortLevel.cs new file mode 100644 index 00000000..be119aa3 --- /dev/null +++ b/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeEffortLevel.cs @@ -0,0 +1,28 @@ +namespace CrestApps.Core.AI.Claude.Models; + +/// +/// The reasoning effort level for Anthropic extended thinking. +/// Maps to the budget_tokens parameter. +/// +public enum ClaudeEffortLevel +{ + /// + /// No effort level specified; the API default is used. + /// + None = 0, + + /// + /// Low reasoning effort. + /// + Low = 1, + + /// + /// Medium reasoning effort. + /// + Medium = 2, + + /// + /// High reasoning effort. + /// + High = 3, +} diff --git a/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeSessionMetadata.cs b/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeSessionMetadata.cs index 8a3772b1..a1dacfe5 100644 --- a/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeSessionMetadata.cs +++ b/src/Primitives/CrestApps.Core.AI.Claude/Models/ClaudeSessionMetadata.cs @@ -10,4 +10,9 @@ public sealed class ClaudeSessionMetadata /// The Anthropic model override for the session. /// public string ClaudeModel { get; set; } + + /// + /// The reasoning effort level for the session. + /// + public ClaudeEffortLevel EffortLevel { get; set; } } diff --git a/src/Primitives/CrestApps.Core.AI.Claude/Services/ClaudeOrchestrator.cs b/src/Primitives/CrestApps.Core.AI.Claude/Services/ClaudeOrchestrator.cs index a5ed191b..9f4cc220 100644 --- a/src/Primitives/CrestApps.Core.AI.Claude/Services/ClaudeOrchestrator.cs +++ b/src/Primitives/CrestApps.Core.AI.Claude/Services/ClaudeOrchestrator.cs @@ -60,6 +60,7 @@ public async IAsyncEnumerable ExecuteStreamingAsync( var modelId = !string.IsNullOrWhiteSpace(metadata?.ClaudeModel) ? metadata.ClaudeModel : null; + var effortLevel = metadata?.EffortLevel ?? ClaudeEffortLevel.None; var anthropicOptions = _anthropicOptions.Value; modelId ??= anthropicOptions.DefaultModel; @@ -90,7 +91,7 @@ public async IAsyncEnumerable ExecuteStreamingAsync( }); var configuredClient = builder.Build(context.ServiceProvider); - var chatOptions = BuildChatOptions(context.CompletionContext, modelId); + var chatOptions = BuildChatOptions(context.CompletionContext, modelId, effortLevel); var prompts = BuildPrompts(context); ChatResponseUpdate errorResponse = null; @@ -134,9 +135,9 @@ private static bool IsConfigured(ClaudeOptions options) !string.IsNullOrWhiteSpace(options.DefaultModel); } - private static ChatOptions BuildChatOptions(AICompletionContext context, string modelId) + private static ChatOptions BuildChatOptions(AICompletionContext context, string modelId, ClaudeEffortLevel effortLevel) { - return new ChatOptions + var options = new ChatOptions { ModelId = modelId, Temperature = context.Temperature, @@ -145,6 +146,25 @@ private static ChatOptions BuildChatOptions(AICompletionContext context, string PresencePenalty = context.PresencePenalty, MaxOutputTokens = context.MaxTokens, }; + + if (effortLevel != ClaudeEffortLevel.None) + { + var effortValue = effortLevel switch + { + ClaudeEffortLevel.Low => "low", + ClaudeEffortLevel.Medium => "medium", + ClaudeEffortLevel.High => "high", + _ => null, + }; + + if (effortValue is not null) + { + options.AdditionalProperties ??= []; + options.AdditionalProperties["reasoning_effort"] = effortValue; + } + } + + return options; } private static List BuildPrompts(OrchestrationContext context) diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Handlers/CopilotChatInteractionSettingsHandler.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Handlers/CopilotChatInteractionSettingsHandler.cs index acd7c2b1..ba956057 100644 --- a/src/Primitives/CrestApps.Core.AI.Copilot/Handlers/CopilotChatInteractionSettingsHandler.cs +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Handlers/CopilotChatInteractionSettingsHandler.cs @@ -26,6 +26,7 @@ public Task UpdatingAsync(ChatInteraction interaction, JsonElement settings) { metadata.CopilotModel = copilotModel; metadata.IsAllowAll = isAllowAll; + metadata.ReasoningEffort = GetEnum(settings, "copilotReasoningEffort"); }); return Task.CompletedTask; } @@ -62,4 +63,22 @@ private static bool GetBool(JsonElement element, string propertyName) return false; } + + private static T GetEnum(JsonElement element, string propertyName) where T : struct, Enum + { + if (element.TryGetProperty(propertyName, out var prop)) + { + if (prop.ValueKind == JsonValueKind.Number && Enum.IsDefined(typeof(T), prop.GetInt32())) + { + return (T)(object)prop.GetInt32(); + } + + if (prop.ValueKind == JsonValueKind.String && Enum.TryParse(prop.GetString(), true, out var result)) + { + return result; + } + } + + return default; + } } diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotModelInfo.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotModelInfo.cs index 6a0036d3..a8752272 100644 --- a/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotModelInfo.cs +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotModelInfo.cs @@ -14,4 +14,10 @@ public sealed class CopilotModelInfo /// The display name of the model. /// public string Name { get; set; } + + /// + /// The premium request cost multiplier (e.g., 1 for standard, 0.33 for discounted, 3 for premium). + /// A value of 0 means unknown. + /// + public double CostMultiplier { get; set; } } diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotReasoningEffort.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotReasoningEffort.cs new file mode 100644 index 00000000..92668ad2 --- /dev/null +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotReasoningEffort.cs @@ -0,0 +1,27 @@ +namespace CrestApps.Core.AI.Copilot.Models; + +/// +/// The reasoning effort level for GitHub Copilot sessions. +/// +public enum CopilotReasoningEffort +{ + /// + /// No explicit effort level; use the model default. + /// + None = 0, + + /// + /// Low reasoning effort. + /// + Low = 1, + + /// + /// Medium reasoning effort. + /// + Medium = 2, + + /// + /// High reasoning effort. + /// + High = 3, +} diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotSessionMetadata.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotSessionMetadata.cs index b9f9906a..a4019971 100644 --- a/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotSessionMetadata.cs +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Models/CopilotSessionMetadata.cs @@ -40,4 +40,9 @@ public sealed class CopilotSessionMetadata /// When the access token expires. /// public DateTime? ExpiresAt { get; set; } + + /// + /// The reasoning effort level for the session. + /// + public CopilotReasoningEffort ReasoningEffort { get; set; } } diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Services/CopilotOrchestrator.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Services/CopilotOrchestrator.cs index a39a8ca6..3d611607 100644 --- a/src/Primitives/CrestApps.Core.AI.Copilot/Services/CopilotOrchestrator.cs +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Services/CopilotOrchestrator.cs @@ -103,6 +103,7 @@ public async IAsyncEnumerable ExecuteStreamingAsync(Orchestr { metadata = md; sessionConfig.Model = metadata.CopilotModel; + sessionConfig.ReasoningEffort = GetReasoningEffortValue(metadata.ReasoningEffort); sessionConfig.OnPermissionRequest = CreatePermissionRequestHandler(metadata.IsAllowAll); } @@ -267,6 +268,17 @@ private static PermissionRequestHandler CreatePermissionRequestHandler(bool allo return (request, invocation) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser, }); } + private static string GetReasoningEffortValue(CopilotReasoningEffort reasoningEffort) + { + return reasoningEffort switch + { + CopilotReasoningEffort.Low => "low", + CopilotReasoningEffort.Medium => "medium", + CopilotReasoningEffort.High => "high", + _ => null, + }; + } + /// /// Configures the BYOK provider on the session config using the options. /// diff --git a/src/Primitives/CrestApps.Core.AI.Copilot/Services/GitHubOAuthService.cs b/src/Primitives/CrestApps.Core.AI.Copilot/Services/GitHubOAuthService.cs index b39dcc66..aab60fc6 100644 --- a/src/Primitives/CrestApps.Core.AI.Copilot/Services/GitHubOAuthService.cs +++ b/src/Primitives/CrestApps.Core.AI.Copilot/Services/GitHubOAuthService.cs @@ -283,6 +283,7 @@ public async Task> ListModelsAsync( { Id = m.Id, Name = !string.IsNullOrEmpty(m.Name) ? m.Name : m.Id, + CostMultiplier = m.Billing?.Multiplier ?? 0, }) .ToList(); } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs index 43a140f1..e39dc6d6 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs @@ -249,7 +249,7 @@ private async Task PopulateDropdownsAsync(AIProfileViewModel model) var cred = await _oauthService.GetCredentialAsync(userId); model.CopilotGitHubUsername = cred?.GitHubUsername; var models = await _oauthService.ListModelsAsync(userId); - model.CopilotAvailableModels = models.Select(m => new SelectListItem(m.Name, m.Id)).ToList(); + model.CopilotAvailableModels = models.Select(m => new SelectListItem(FormatCopilotModelName(m), m.Id)).ToList(); } } } @@ -428,7 +428,7 @@ private static void ApplyTemplateToProfile(AIProfile profile, AIProfileTemplate if (template.TryGet(out var copilotMetadata)) { - profile.Put(new CopilotSessionMetadata { CopilotModel = copilotMetadata.CopilotModel, IsAllowAll = copilotMetadata.IsAllowAll, }); + profile.Put(new CopilotSessionMetadata { CopilotModel = copilotMetadata.CopilotModel, ReasoningEffort = copilotMetadata.ReasoningEffort, IsAllowAll = copilotMetadata.IsAllowAll, }); } else { @@ -437,7 +437,7 @@ private static void ApplyTemplateToProfile(AIProfile profile, AIProfileTemplate if (template.TryGet(out var anthropicMetadata)) { - profile.Put(new ClaudeSessionMetadata { ClaudeModel = anthropicMetadata.ClaudeModel, }); + profile.Put(new ClaudeSessionMetadata { ClaudeModel = anthropicMetadata.ClaudeModel, EffortLevel = anthropicMetadata.EffortLevel, }); } else { @@ -486,4 +486,13 @@ private async Task PopulateClaudeModelsAsync(AIProfileViewModel model) } private bool IsCopilotConfigured() => _copilotOptions.IsConfigured(); + + private static string FormatCopilotModelName(CopilotModelInfo model) + { + var name = !string.IsNullOrWhiteSpace(model.Name) ? model.Name : model.Id; + + return model.CostMultiplier > 0 + ? $"{name} (x{model.CostMultiplier.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)})" + : name; + } } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs index f0490d1b..bab3194a 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs @@ -1,3 +1,4 @@ +using System.Globalization; using CrestApps.Core.AI; using CrestApps.Core.AI.A2A.Models; using CrestApps.Core.AI.Claude.Models; @@ -337,8 +338,17 @@ private async Task PopulateCopilotStatusAsync(AITemplateViewModel model) var credential = await _oauthService.GetCredentialAsync(userId); model.CopilotGitHubUsername = credential?.GitHubUsername; var models = await _oauthService.ListModelsAsync(userId); - model.CopilotAvailableModels = models.Select(m => new SelectListItem(m.Name, m.Id)).ToList(); + model.CopilotAvailableModels = models.Select(m => new SelectListItem(FormatCopilotModelName(m), m.Id)).ToList(); } private bool IsCopilotConfigured() => _copilotOptions.IsConfigured(); + + private static string FormatCopilotModelName(CopilotModelInfo model) + { + var name = !string.IsNullOrWhiteSpace(model.Name) ? model.Name : model.Id; + + return model.CostMultiplier > 0 + ? $"{name} (x{model.CostMultiplier.ToString("0.##", CultureInfo.InvariantCulture)})" + : name; + } } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs index 4bad49a4..e97de9e2 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AIProfileViewModel.cs @@ -138,11 +138,15 @@ public sealed class AIProfileViewModel // Anthropic public string ClaudeModel { get; set; } + public ClaudeEffortLevel ClaudeEffortLevel { get; set; } + public bool ClaudeIsConfigured { get; set; } // Copilot public string CopilotModel { get; set; } + public CopilotReasoningEffort CopilotReasoningEffort { get; set; } + public bool CopilotIsAllowAll { get; set; } public bool CopilotIsConfigured { get; set; } @@ -337,12 +341,14 @@ public static AIProfileViewModel FromProfile(AIProfile profile) if (profile.TryGet(out var copilotMeta)) { vm.CopilotModel = copilotMeta.CopilotModel; + vm.CopilotReasoningEffort = copilotMeta.ReasoningEffort; vm.CopilotIsAllowAll = copilotMeta.IsAllowAll; } if (profile.TryGet(out var anthropicMeta)) { vm.ClaudeModel = anthropicMeta.ClaudeModel; + vm.ClaudeEffortLevel = anthropicMeta.EffortLevel; } return vm; @@ -549,6 +555,7 @@ public void ApplyTo(AIProfile profile) profile.Alter(metadata => { metadata.ClaudeModel = ClaudeModel; + metadata.EffortLevel = ClaudeEffortLevel; }); } else @@ -563,6 +570,7 @@ public void ApplyTo(AIProfile profile) profile.Alter(metadata => { metadata.CopilotModel = CopilotModel; + metadata.ReasoningEffort = CopilotReasoningEffort; metadata.IsAllowAll = CopilotIsAllowAll; }); } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs index 0985a136..acd5b2b9 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/ViewModels/AITemplateViewModel.cs @@ -120,6 +120,8 @@ public sealed class AITemplateViewModel // Anthropic. public string ClaudeModel { get; set; } + public ClaudeEffortLevel ClaudeEffortLevel { get; set; } + public bool ClaudeIsConfigured { get; set; } // Settings. @@ -129,6 +131,8 @@ public sealed class AITemplateViewModel // Copilot. public string CopilotModel { get; set; } + public CopilotReasoningEffort CopilotReasoningEffort { get; set; } + public bool CopilotIsAllowAll { get; set; } public bool CopilotIsConfigured { get; set; } @@ -312,12 +316,14 @@ public static AITemplateViewModel FromTemplate(AIProfileTemplate template) if (template.TryGet(out var copilotMetadata)) { model.CopilotModel = copilotMetadata.CopilotModel; + model.CopilotReasoningEffort = copilotMetadata.ReasoningEffort; model.CopilotIsAllowAll = copilotMetadata.IsAllowAll; } if (template.TryGet(out var anthropicMetadata)) { model.ClaudeModel = anthropicMetadata.ClaudeModel; + model.ClaudeEffortLevel = anthropicMetadata.EffortLevel; } } @@ -522,6 +528,7 @@ public void ApplyTo(AIProfileTemplate template) template.Put(new ClaudeSessionMetadata { ClaudeModel = ClaudeModel, + EffortLevel = ClaudeEffortLevel, }); } else @@ -535,6 +542,7 @@ public void ApplyTo(AIProfileTemplate template) template.Put(new CopilotSessionMetadata { CopilotModel = CopilotModel, + ReasoningEffort = CopilotReasoningEffort, IsAllowAll = CopilotIsAllowAll, }); } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml index 05310b44..8022de28 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Create.cshtml @@ -161,6 +161,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -168,6 +169,7 @@ { "ClaudePrefix", "ap" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml index 7856daff..c022a33e 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AIProfile/Edit.cshtml @@ -217,6 +217,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -224,6 +225,7 @@ { "ClaudePrefix", "ap" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml index 45d191fa..55a24d8d 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Create.cshtml @@ -151,6 +151,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -159,6 +160,7 @@ { "ClaudePrefix", "at" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml index 17d82871..5a7800f3 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Views/AITemplate/Edit.cshtml @@ -152,6 +152,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -160,6 +161,7 @@ { "ClaudePrefix", "at" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/CopilotAuthController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/CopilotAuthController.cs index 37ef4d2a..5a8e3d29 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/CopilotAuthController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/CopilotAuthController.cs @@ -114,7 +114,7 @@ public async Task Models() var models = await _oauthService.ListModelsAsync(userId); - return Json(models.Select(m => new { m.Id, m.Name })); + return Json(models.Select(m => new { m.Id, m.Name, m.CostMultiplier })); } [HttpPost] diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs index 822ea68d..b862f90c 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs @@ -237,6 +237,7 @@ public async Task Chat(string id) DataSourceIsInScope = ragMetadata?.IsInScope ?? false, DataSourceFilter = ragMetadata?.Filter, ClaudeModel = anthropicMetadata?.ClaudeModel, + ClaudeEffortLevel = anthropicMetadata?.EffortLevel ?? CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel.None, SelectedA2AConnectionIds = interaction.A2AConnectionIds?.ToArray() ?? [], SelectedMcpConnectionIds = interaction.McpConnectionIds?.ToArray() ?? [], SelectedToolNames = interaction.ToolNames?.ToArray() ?? [], @@ -581,6 +582,7 @@ private async Task ApplyMetadataAsync(ChatInteraction interaction, ChatInteracti interaction.Alter(metadata => { metadata.ClaudeModel = model.ClaudeModel; + metadata.EffortLevel = model.ClaudeEffortLevel; }); } else @@ -593,6 +595,7 @@ private async Task ApplyMetadataAsync(ChatInteraction interaction, ChatInteracti interaction.Alter(metadata => { metadata.CopilotModel = model.CopilotModel; + metadata.ReasoningEffort = model.CopilotReasoningEffort; metadata.IsAllowAll = model.CopilotIsAllowAll; }); } @@ -771,7 +774,7 @@ private async Task PopulateCopilotStatusAsync(ChatInteractionViewModel model) model.CopilotGitHubUsername = cred?.GitHubUsername; var models = await _oauthService.ListModelsAsync(userId); model.CopilotAvailableModels = models - .Select(m => new SelectListItem(m.Name, m.Id)) + .Select(m => new SelectListItem(FormatCopilotModelName(m), m.Id)) .ToList(); } } @@ -816,7 +819,7 @@ private async Task PopulateCopilotChatStatusAsync(ChatInteractionChatViewModel m model.CopilotGitHubUsername = cred?.GitHubUsername; var models = await _oauthService.ListModelsAsync(userId); model.CopilotAvailableModels = models - .Select(m => new SelectListItem(m.Name, m.Id)) + .Select(m => new SelectListItem(FormatCopilotModelName(m), m.Id)) .ToList(); } } @@ -828,7 +831,7 @@ private async Task PopulateCopilotChatStatusAsync(ChatInteractionChatViewModel m if (interaction != null && interaction.TryGet(out var copilotMeta)) { model.CopilotModel = copilotMeta.CopilotModel; - + model.CopilotReasoningEffort = copilotMeta.ReasoningEffort; model.CopilotIsAllowAll = copilotMeta.IsAllowAll; } } @@ -847,4 +850,13 @@ private async Task PopulateClaudeModelsAsync(ChatInteractionChatViewModel model) } private bool IsCopilotConfigured() => _copilotOptions.IsConfigured(); + + private static string FormatCopilotModelName(CopilotModelInfo model) + { + var name = !string.IsNullOrWhiteSpace(model.Name) ? model.Name : model.Id; + + return model.CostMultiplier > 0 + ? $"{name} (x{model.CostMultiplier.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)})" + : name; + } } diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionChatViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionChatViewModel.cs index f408707a..703e7971 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionChatViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionChatViewModel.cs @@ -75,6 +75,8 @@ internal sealed class ChatInteractionChatViewModel // Copilot public string CopilotModel { get; set; } + public CrestApps.Core.AI.Copilot.Models.CopilotReasoningEffort CopilotReasoningEffort { get; set; } + public bool CopilotIsAllowAll { get; set; } public bool CopilotIsConfigured { get; set; } @@ -88,6 +90,8 @@ internal sealed class ChatInteractionChatViewModel // Anthropic public string ClaudeModel { get; set; } + public CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel ClaudeEffortLevel { get; set; } + public bool ClaudeIsConfigured { get; set; } // Existing messages for the chat diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionViewModel.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionViewModel.cs index 4e818758..48b332a2 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionViewModel.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/ViewModels/ChatInteractionViewModel.cs @@ -62,6 +62,8 @@ public sealed class ChatInteractionViewModel // Copilot public string CopilotModel { get; set; } + public CrestApps.Core.AI.Copilot.Models.CopilotReasoningEffort CopilotReasoningEffort { get; set; } + public bool CopilotIsAllowAll { get; set; } public bool CopilotIsConfigured { get; set; } @@ -75,6 +77,8 @@ public sealed class ChatInteractionViewModel // Anthropic public string ClaudeModel { get; set; } + public CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel ClaudeEffortLevel { get; set; } + public bool ClaudeIsConfigured { get; set; } [BindNever] diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Chat.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Chat.cshtml index 916964ed..d08033de 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Chat.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Chat.cshtml @@ -216,6 +216,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -223,6 +224,7 @@ { "ClaudePrefix", "ci" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Create.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Create.cshtml index fdb2a52e..6536b4db 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Create.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Views/ChatInteraction/Create.cshtml @@ -92,6 +92,7 @@ { "CopilotIsAuth", Model.CopilotIsAuthenticated }, { "CopilotUsername", Model.CopilotGitHubUsername }, { "CopilotModel", Model.CopilotModel }, + { "CopilotReasoningEffort", Model.CopilotReasoningEffort }, { "CopilotIsAllowAll", Model.CopilotIsAllowAll }, { "CopilotModels", Model.CopilotAvailableModels }, }) @@ -99,6 +100,7 @@ { "ClaudePrefix", "ci" }, { "ClaudeIsConfigured", Model.ClaudeIsConfigured }, { "ClaudeModel", Model.ClaudeModel }, + { "ClaudeEffortLevel", Model.ClaudeEffortLevel }, { "ClaudeModels", Model.AnthropicAvailableModels }, }) diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_ClaudeConfig.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_ClaudeConfig.cshtml index 47174acb..69e40642 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_ClaudeConfig.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_ClaudeConfig.cshtml @@ -3,6 +3,7 @@ var isConfigured = (bool)(ViewData["ClaudeIsConfigured"] ?? false); var anthropicModel = ViewData["ClaudeModel"] as string ?? ""; var anthropicModels = ViewData["ClaudeModels"] as IEnumerable ?? []; + var effortLevel = (CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel)(ViewData["ClaudeEffortLevel"] ?? CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel.None); var orchestratorName = CrestApps.Core.AI.Claude.Services.ClaudeOrchestrator.OrchestratorName; } @@ -22,7 +23,7 @@ } else if (anthropicModels.Any()) { -
+
Select a Claude model override for this item, or leave it on the configured default.
+ +
+ + +
Select the reasoning effort level. Higher effort produces more thorough responses but uses more tokens.
+
} else { -
+
Optionally override the Claude model. Leave empty to use the default model configured in settings.
+ +
+ + +
Select the reasoning effort level. Higher effort produces more thorough responses but uses more tokens.
+
}
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_CopilotConfig.cshtml b/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_CopilotConfig.cshtml index 9f6714a9..b6560aa0 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_CopilotConfig.cshtml +++ b/src/Startup/CrestApps.Core.Mvc.Web/Views/Shared/_CopilotConfig.cshtml @@ -8,6 +8,7 @@ - CopilotIsAuthenticated (bool) - CopilotGitHubUsername (string) - CopilotModel (string) + - CopilotReasoningEffort (enum) - CopilotIsAllowAll (bool) - CopilotAvailableModels (List) *@ @@ -21,6 +22,7 @@ var isAuth = (bool)(ViewData["CopilotIsAuth"] ?? false); var username = ViewData["CopilotUsername"] as string ?? ""; var copilotModel = ViewData["CopilotModel"] as string ?? ""; + var copilotReasoningEffort = (CrestApps.Core.AI.Copilot.Models.CopilotReasoningEffort)(ViewData["CopilotReasoningEffort"] ?? CrestApps.Core.AI.Copilot.Models.CopilotReasoningEffort.None); var isAllowAll = (bool)(ViewData["CopilotIsAllowAll"] ?? false); var copilotModels = ViewData["CopilotModels"] as List ?? []; var orchestratorName = CrestApps.Core.AI.Copilot.Services.CopilotOrchestrator.OrchestratorName; @@ -91,6 +93,17 @@
Select the Copilot model to use.
+
+ + +
Select the reasoning effort level for the Copilot model.
+
+
Optionally override the model name. Leave empty to use the default model configured in Copilot settings.
+
+ + +
Select the reasoning effort level for the Copilot model.
+
+
0 ? m.name + ' (x' + costMultiplier + ')' : m.name; modelSelect.appendChild(opt); }); if (currentValue && modelSelect.querySelector('option[value="' + CSS.escape(currentValue) + '"]')) {