using MarketAlly.AIPlugin; using MarketAlly.AIPlugin.Agentic; using MarketAlly.AIPlugin.Agentic.Models; using MarketAlly.AIPlugin.Conversation; using MarketAlly.GitCommitEditor.Options; namespace MarketAlly.GitCommitEditor.Services; /// /// Service for managing AI model providers dynamically based on configured API keys. /// Only shows providers that the user has actually configured. /// public interface IModelProviderService { /// /// Gets the providers that have API keys configured. /// IReadOnlyList AvailableProviders { get; } /// /// Gets models for a provider that support function calling. /// IReadOnlyList GetModelsForProvider(string provider); /// /// Gets models with display information for a provider. /// IReadOnlyList GetModelsWithInfoForProvider(string provider); /// /// Gets model info by ID. /// ModelDisplayInfo? GetModelInfo(string modelId); /// /// Gets the API key for a provider. /// string? GetApiKey(AIProvider provider); /// /// Checks if a provider has an API key configured. /// bool HasApiKey(AIProvider provider); /// /// Refreshes the available providers based on current API key configuration. /// Call this after adding or removing an API key. /// void RefreshProviders(); } /// /// Implementation of IModelProviderService that uses IApiKeyProvider to determine available providers. /// Uses the static ModelLibrary.Default for efficient querying. /// public class ModelProviderService : IModelProviderService { private readonly IApiKeyProvider _apiKeyProvider; public ModelProviderService(IApiKeyProvider apiKeyProvider) { _apiKeyProvider = apiKeyProvider; } public IReadOnlyList AvailableProviders => ModelLibrary.Default.GetModelsWithFunctionCalling() .Where(m => m.Type != ModelType.Legacy) .Select(m => m.Provider.ToString()) .Distinct() .OrderBy(p => p) .ToList(); public void RefreshProviders() { // No-op - static library doesn't need refreshing // API key changes are handled separately by IApiKeyProvider } public IReadOnlyList GetModelsForProvider(string provider) { var aiProvider = ParseProvider(provider); var models = ModelLibrary.Default.GetModelsByProvider(aiProvider) .Where(m => m.SupportsFunctionCalling && m.Type != ModelType.Legacy) .OrderByDescending(m => m.Tier) .ThenBy(m => m.DisplayName) .Select(m => m.ModelId) .ToList(); return models.Count > 0 ? models : new[] { ModelConstants.Claude.Sonnet4 }; } public IReadOnlyList GetModelsWithInfoForProvider(string provider) { var aiProvider = ParseProvider(provider); var models = ModelLibrary.Default.GetModelsByProvider(aiProvider) .Where(m => m.SupportsFunctionCalling && m.Type != ModelType.Legacy) .OrderByDescending(m => m.Tier) .ThenBy(m => m.DisplayName) .Select(m => new ModelDisplayInfo { ModelId = m.ModelId, DisplayName = m.DisplayName, Tier = m.Tier.ToString(), InputCostPer1MTokens = m.InputCostPer1MTokens, OutputCostPer1MTokens = m.OutputCostPer1MTokens, Speed = m.Speed.ToString() }) .ToList(); return models.Count > 0 ? models : GetDefaultModelInfo(); } public ModelDisplayInfo? GetModelInfo(string modelId) { var model = ModelLibrary.Default.GetModel(modelId); if (model == null) return null; return new ModelDisplayInfo { ModelId = model.ModelId, DisplayName = model.DisplayName, Tier = model.Tier.ToString(), InputCostPer1MTokens = model.InputCostPer1MTokens, OutputCostPer1MTokens = model.OutputCostPer1MTokens, Speed = model.Speed.ToString() }; } public string? GetApiKey(AIProvider provider) => _apiKeyProvider.GetApiKey(provider); public bool HasApiKey(AIProvider provider) => _apiKeyProvider.HasApiKey(provider); private static IReadOnlyList GetDefaultModelInfo() => new[] { new ModelDisplayInfo { ModelId = ModelConstants.Claude.Sonnet4, DisplayName = "Claude Sonnet 4", Tier = "Balanced", InputCostPer1MTokens = 3.00m, OutputCostPer1MTokens = 15.00m, Speed = "Fast" } }; private static AIProvider ParseProvider(string? provider) { return provider?.ToLowerInvariant() switch { "claude" or "anthropic" => AIProvider.Claude, "openai" or "gpt" => AIProvider.OpenAI, "gemini" or "google" => AIProvider.Gemini, "qwen" or "alibaba" => AIProvider.Qwen, _ => AIProvider.Claude }; } }