Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public Task UpdatingAsync(ChatInteraction interaction, JsonElement settings)
interaction.Alter<ClaudeSessionMetadata>(metadata =>
{
metadata.ClaudeModel = GetString(settings, "anthropicModel");
metadata.EffortLevel = GetEnum<ClaudeEffortLevel>(settings, "anthropicEffortLevel");
});

return Task.CompletedTask;
Expand All @@ -38,4 +39,22 @@ private static string GetString(JsonElement element, string propertyName)

return null;
}

private static T GetEnum<T>(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<T>(prop.GetString(), ignoreCase: true, out var result))
{
return result;
}
}

return default;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace CrestApps.Core.AI.Claude.Models;

/// <summary>
/// The reasoning effort level for Anthropic extended thinking.
/// Maps to the <c>budget_tokens</c> parameter.
/// </summary>
public enum ClaudeEffortLevel
{
/// <summary>
/// No effort level specified; the API default is used.
/// </summary>
None = 0,

/// <summary>
/// Low reasoning effort.
/// </summary>
Low = 1,

/// <summary>
/// Medium reasoning effort.
/// </summary>
Medium = 2,

/// <summary>
/// High reasoning effort.
/// </summary>
High = 3,
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ public sealed class ClaudeSessionMetadata
/// The Anthropic model override for the session.
/// </summary>
public string ClaudeModel { get; set; }

/// <summary>
/// The reasoning effort level for the session.
/// </summary>
public ClaudeEffortLevel EffortLevel { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> ExecuteStreamingAsync(
var modelId = !string.IsNullOrWhiteSpace(metadata?.ClaudeModel)
? metadata.ClaudeModel
: null;
var effortLevel = metadata?.EffortLevel ?? ClaudeEffortLevel.None;
var anthropicOptions = _anthropicOptions.Value;
modelId ??= anthropicOptions.DefaultModel;

Expand Down Expand Up @@ -90,7 +91,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> 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;

Expand Down Expand Up @@ -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,
Expand All @@ -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<ChatMessage> BuildPrompts(OrchestrationContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public Task UpdatingAsync(ChatInteraction interaction, JsonElement settings)
{
metadata.CopilotModel = copilotModel;
metadata.IsAllowAll = isAllowAll;
metadata.ReasoningEffort = GetEnum<CopilotReasoningEffort>(settings, "copilotReasoningEffort");
});
return Task.CompletedTask;
}
Expand Down Expand Up @@ -62,4 +63,22 @@ private static bool GetBool(JsonElement element, string propertyName)

return false;
}

private static T GetEnum<T>(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<T>(prop.GetString(), true, out var result))
{
return result;
}
}

return default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ public sealed class CopilotModelInfo
/// The display name of the model.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The premium request cost multiplier (e.g., 1 for standard, 0.33 for discounted, 3 for premium).
/// A value of <c>0</c> means unknown.
/// </summary>
public double CostMultiplier { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace CrestApps.Core.AI.Copilot.Models;

/// <summary>
/// The reasoning effort level for GitHub Copilot sessions.
/// </summary>
public enum CopilotReasoningEffort
{
/// <summary>
/// No explicit effort level; use the model default.
/// </summary>
None = 0,

/// <summary>
/// Low reasoning effort.
/// </summary>
Low = 1,

/// <summary>
/// Medium reasoning effort.
/// </summary>
Medium = 2,

/// <summary>
/// High reasoning effort.
/// </summary>
High = 3,
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public sealed class CopilotSessionMetadata
/// When the access token expires.
/// </summary>
public DateTime? ExpiresAt { get; set; }

/// <summary>
/// The reasoning effort level for the session.
/// </summary>
public CopilotReasoningEffort ReasoningEffort { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> ExecuteStreamingAsync(Orchestr
{
metadata = md;
sessionConfig.Model = metadata.CopilotModel;
sessionConfig.ReasoningEffort = GetReasoningEffortValue(metadata.ReasoningEffort);
sessionConfig.OnPermissionRequest = CreatePermissionRequestHandler(metadata.IsAllowAll);
}

Expand Down Expand Up @@ -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,
};
}

/// <summary>
/// Configures the BYOK provider on the session config using the options.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ public async Task<IReadOnlyCollection<CopilotModelInfo>> ListModelsAsync(
{
Id = m.Id,
Name = !string.IsNullOrEmpty(m.Name) ? m.Name : m.Id,
CostMultiplier = m.Billing?.Multiplier ?? 0,
})
.ToList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Expand Down Expand Up @@ -428,7 +428,7 @@ private static void ApplyTemplateToProfile(AIProfile profile, AIProfileTemplate

if (template.TryGet<CopilotSessionMetadata>(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
{
Expand All @@ -437,7 +437,7 @@ private static void ApplyTemplateToProfile(AIProfile profile, AIProfileTemplate

if (template.TryGet<ClaudeSessionMetadata>(out var anthropicMetadata))
{
profile.Put(new ClaudeSessionMetadata { ClaudeModel = anthropicMetadata.ClaudeModel, });
profile.Put(new ClaudeSessionMetadata { ClaudeModel = anthropicMetadata.ClaudeModel, EffortLevel = anthropicMetadata.EffortLevel, });
}
else
{
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using CrestApps.Core.AI;
using CrestApps.Core.AI.A2A.Models;
using CrestApps.Core.AI.Claude.Models;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -337,12 +341,14 @@ public static AIProfileViewModel FromProfile(AIProfile profile)
if (profile.TryGet<CopilotSessionMetadata>(out var copilotMeta))
{
vm.CopilotModel = copilotMeta.CopilotModel;
vm.CopilotReasoningEffort = copilotMeta.ReasoningEffort;
vm.CopilotIsAllowAll = copilotMeta.IsAllowAll;
}

if (profile.TryGet<ClaudeSessionMetadata>(out var anthropicMeta))
{
vm.ClaudeModel = anthropicMeta.ClaudeModel;
vm.ClaudeEffortLevel = anthropicMeta.EffortLevel;
}

return vm;
Expand Down Expand Up @@ -549,6 +555,7 @@ public void ApplyTo(AIProfile profile)
profile.Alter<ClaudeSessionMetadata>(metadata =>
{
metadata.ClaudeModel = ClaudeModel;
metadata.EffortLevel = ClaudeEffortLevel;
});
}
else
Expand All @@ -563,6 +570,7 @@ public void ApplyTo(AIProfile profile)
profile.Alter<CopilotSessionMetadata>(metadata =>
{
metadata.CopilotModel = CopilotModel;
metadata.ReasoningEffort = CopilotReasoningEffort;
metadata.IsAllowAll = CopilotIsAllowAll;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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; }
Expand Down Expand Up @@ -312,12 +316,14 @@ public static AITemplateViewModel FromTemplate(AIProfileTemplate template)
if (template.TryGet<CopilotSessionMetadata>(out var copilotMetadata))
{
model.CopilotModel = copilotMetadata.CopilotModel;
model.CopilotReasoningEffort = copilotMetadata.ReasoningEffort;
model.CopilotIsAllowAll = copilotMetadata.IsAllowAll;
}

if (template.TryGet<ClaudeSessionMetadata>(out var anthropicMetadata))
{
model.ClaudeModel = anthropicMetadata.ClaudeModel;
model.ClaudeEffortLevel = anthropicMetadata.EffortLevel;
}
}

Expand Down Expand Up @@ -522,6 +528,7 @@ public void ApplyTo(AIProfileTemplate template)
template.Put(new ClaudeSessionMetadata
{
ClaudeModel = ClaudeModel,
EffortLevel = ClaudeEffortLevel,
});
}
else
Expand All @@ -535,6 +542,7 @@ public void ApplyTo(AIProfileTemplate template)
template.Put(new CopilotSessionMetadata
{
CopilotModel = CopilotModel,
ReasoningEffort = CopilotReasoningEffort,
IsAllowAll = CopilotIsAllowAll,
});
}
Expand Down
Loading
Loading