{"repo": "alal76/crm-solution", "files": {"CRM.Backend/src/CRM.Infrastructure/Services/LLMService.cs": {"9cfd309fb2e83d4adc5e8a4af6750311b1192c21": {"t": 1785746194.3837702, "n": 66933, "h": "3ee8a7f7fde58a3f", "c": "// CRM Solution - Customer Relationship Management System\n// Copyright (C) 2024-2026 Abhishek Lal\n//\n// This software is source-available. Non-commercial use is permitted under\n// the terms of the LICENSE file. Commercial use requires a separate license.\n// See the LICENSE file in the root directory for full terms.\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing CRM.Core.Interfaces;\nusing CRM.Infrastructure.Services.LLM;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// \n/// Configuration for LLM providers\n/// \npublic class LLMProviderOptions\n{\n public string DefaultProvider { get; set; } = \"openai\";\n public OpenAIOptions OpenAI { get; set; } = new();\n public AzureOpenAIOptions AzureOpenAI { get; set; } = new();\n public AnthropicOptions Anthropic { get; set; } = new();\n public GoogleCloudOptions GoogleCloud { get; set; } = new();\n public AWSBedrockOptions AWSBedrock { get; set; } = new();\n public DeepSeekOptions DeepSeek { get; set; } = new();\n public GroqOptions Groq { get; set; } = new();\n public AllenAIOptions AllenAI { get; set; } = new();\n public LocalLLMOptions LocalLLM { get; set; } = new();\n public CustomEndpointOptions CustomEndpoint { get; set; } = new();\n public int DefaultMaxTokens { get; set; } = 1000;\n public double DefaultTemperature { get; set; } = 0.7;\n public int TimeoutSeconds { get; set; } = 60;\n public int MaxRetries { get; set; } = 3;\n public bool EnableFallback { get; set; } = true;\n public string[] FallbackOrder { get; set; } = { \"openai\", \"anthropic\", \"azure\", \"google\", \"allenai\", \"groq\", \"local\" };\n}\n\npublic class OpenAIOptions\n{\n public string ApiKey { get; set; } = \"\";\n public string BaseUrl { get; set; } = \"https://api.openai.com/v1\";\n public string DefaultModel { get; set; } = \"gpt-4\";\n public string Organization { get; set; } = \"\";\n}\n\n/// \n/// Azure OpenAI Service configuration\n/// \npublic class AzureOpenAIOptions\n{\n public string Endpoint { get; set; } = \"\"; // e.g., https://your-resource.openai.azure.com\n public string ApiKey { get; set; } = \"\";\n public string DeploymentName { get; set; } = \"\"; // Your deployed model name\n public string ApiVersion { get; set; } = \"2024-02-01\";\n public string DefaultModel { get; set; } = \"gpt-4\";\n}\n\npublic class AnthropicOptions\n{\n public string ApiKey { get; set; } = \"\";\n public string BaseUrl { get; set; } = \"https://api.anthropic.com/v1\";\n public string DefaultModel { get; set; } = \"claude-3-sonnet-20240229\";\n public string ApiVersion { get; set; } = \"2023-06-01\";\n}\n\n/// \n/// Google Cloud Vertex AI / Gemini configuration\n/// \npublic class GoogleCloudOptions\n{\n public string ProjectId { get; set; } = \"\";\n public string Location { get; set; } = \"us-central1\";\n public string ApiKey { get; set; } = \"\"; // For Gemini API\n public string ServiceAccountKeyPath { get; set; } = \"\"; // For Vertex AI (JSON key file)\n public string DefaultModel { get; set; } = \"gemini-1.5-pro\";\n public bool UseVertexAI { get; set; } = false; // true = Vertex AI, false = Gemini API\n}\n\n/// \n/// AWS Bedrock configuration\n/// \npublic class AWSBedrockOptions\n{\n public string Region { get; set; } = \"us-east-1\";\n public string AccessKeyId { get; set; } = \"\";\n public string SecretAccessKey { get; set; } = \"\";\n public string SessionToken { get; set; } = \"\"; // Optional, for temporary credentials\n public string DefaultModel { get; set; } = \"anthropic.claude-3-sonnet-20240229-v1:0\";\n public bool UseDefaultCredentials { get; set; } = true; // Use AWS credential chain\n}\n\n/// \n/// DeepSeek AI configuration\n/// \npublic class DeepSeekOptions\n{\n public string ApiKey { get; set; } = \"\";\n public string BaseUrl { get; set; } = \"https://api.deepseek.com\";\n public string DefaultModel { get; set; } = \"deepseek-chat\"; // or deepseek-coder, deepseek-reasoner\n}\n\n/// \n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// \npublic class GroqOptions\n{\n public string ApiKey { get; set; } = \"\";\n public string BaseUrl { get; set; } = \"https://api.groq.com/openai/v1\";\n public string DefaultModel { get; set; } = \"llama-3.3-70b-versatile\";\n}\n\n/// \n/// Allen AI (AI2) configuration - Free open-source models via Hugging Face\n/// Models: OLMo-7B, Tulu-2-7B, and other open research models\n/// \npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// Hugging Face API token (alias: HuggingFaceApiKey)\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// Hugging Face API token (alias for ApiKey, for backwards compatibility)\n public string HuggingFaceApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n public string BaseUrl { get; set; } = \"https://api-inference.huggingface.co/models\";\n public string OLMoEndpoint { get; set; } = \"https://api-inference.huggingface.co/models/allenai/OLMo-7B\";\n public string TuluEndpoint { get; set; } = \"https://api-inference.huggingface.co/models/allenai/tulu-2-7b\";\n public string DefaultModel { get; set; } = \"allenai/OLMo-7B-Instruct\";\n public bool Enabled { get; set; } = true;\n public int MaxNewTokens { get; set; } = 1000;\n public double Temperature { get; set; } = 0.7;\n public int TimeoutSeconds { get; set; } = 60;\n public int MaxRetries { get; set; } = 3;\n public bool EnableCaching { get; set; } = true;\n public int CacheExpirationMinutes { get; set; } = 60;\n public bool EnableLocalFallback { get; set; } = true;\n public int BatchSize { get; set; } = 10;\n}\n\n/// \n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// \npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default // NOSONAR - S5332: internal container-to-container URL, HTTPS not applicable in Docker bridge network\n public string ApiKey { get; set; } = \"\"; // Optional for some local servers\n public string DefaultModel { get; set; } = \"llama3\";\n public string ApiFormat { get; set; } = \"ollama\"; // ollama, openai, or custom\n public Dictionary Headers { get; set; } = new();\n public bool Enabled { get; set; } = false;\n}\n\npublic class CustomEndpointOptions\n{\n public string Url { get; set; } = \"\";\n public string ApiKey { get; set; } = \"\";\n public Dictionary Headers { get; set; } = new();\n}\n\n/// \n/// LLM request parameters\n/// \npublic class LLMRequest\n{\n public string Provider { get; set; } = \"openai\";\n public string Model { get; set; } = \"gpt-4\";\n public string Prompt { get; set; } = \"\";\n public string? SystemPrompt { get; set; }\n public List? Messages { get; set; }\n public double Temperature { get; set; } = 0.7;\n public int MaxTokens { get; set; } = 1000;\n public bool JsonMode { get; set; }\n public string? JsonSchema { get; set; }\n public Dictionary? Variables { get; set; }\n}\n\npublic class LLMMessage\n{\n public string Role { get; set; } = \"user\";\n public string Content { get; set; } = \"\";\n}\n\n/// \n/// LLM response\n/// \npublic class LLMResponse\n{\n public bool Success { get; set; }\n public string Content { get; set; } = \"\";\n public string? Error { get; set; }\n public string Provider { get; set; } = \"\";\n public string Model { get; set; } = \"\";\n public int PromptTokens { get; set; }\n public int CompletionTokens { get; set; }\n public int TotalTokens { get; set; }\n public double DurationMs { get; set; }\n public object? ParsedJson { get; set; }\n}\n\n/// \n/// LLM provider info for frontend\n/// \npublic class LLMProviderInfo\n{\n public string Value { get; set; } = \"\";\n public string Label { get; set; } = \"\";\n public bool IsConfigured { get; set; }\n public List Models { get; set; } = new();\n}\n\n/// \n/// LLM model info for frontend\n/// \npublic class LLMModelInfo\n{\n public string Value { get; set; } = \"\";\n public string Label { get; set; } = \"\";\n public string Provider { get; set; } = \"\";\n public bool IsDefault { get; set; }\n}\n\n/// \n/// Interface for LLM service\n/// \npublic interface ILLMService\n{\n Task CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task IsConfiguredAsync(string provider);\n List GetAvailableProviders();\n List GetAvailableModels();\n}\n\n/// \n/// Multi-provider LLM service with fallback support\n/// \npublic class LLMService : ILLMService\n{\n private readonly ILogger _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n private readonly ILLMSettingsService? _settingsService;\n\n // AP-036: Extracted provider implementations\n private readonly OpenAILLMProvider _openAIProvider;\n private readonly AnthropicLLMProvider _anthropicProvider;\n private readonly LocalLLMProvider _localProvider;\n\n public LLMService(\n ILogger logger,\n IOptions options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = null,\n ILLMSettingsService? settingsService = null)\n {\n _logger = logger;\n _options = options.Value;\n _httpClient = httpClient ?? new HttpClient();\n _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);\n _resilienceService = resilienceService;\n _settingsService = settingsService;\n\n // AP-036: Initialise extracted provider objects using same HttpClient + options\n _openAIProvider = new OpenAILLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);\n _anthropicProvider = new AnthropicLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);\n _localProvider = new LocalLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);\n }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => IsValidApiKey(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => IsValidApiKey(_options.AzureOpenAI.Endpoint) &&\n IsValidApiKey(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => IsValidApiKey(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => IsValidApiKey(_options.GoogleCloud.ApiKey) ||\n IsValidApiKey(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials ||\n (IsValidApiKey(_options.AWSBedrock.AccessKeyId) &&\n IsValidApiKey(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => IsValidApiKey(_options.DeepSeek.ApiKey),\n \"groq\" => IsValidApiKey(_options.Groq.ApiKey),\n \"allenai\" or \"huggingface\" or \"ai2\" => _options.AllenAI.Enabled &&\n IsValidApiKey(_options.AllenAI.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled &&\n IsValidApiKey(_options.LocalLLM.BaseUrl),\n \"custom\" => IsValidApiKey(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n /// \n /// Async version of IsConfigured that also checks for API keys stored in the database.\n /// Use this when you need to check if a provider is truly configured (DB + config combined).\n /// \n public async Task IsConfiguredAsync(string provider)\n {\n // First check config (fast path)\n if (IsConfigured(provider))\n {\n return true;\n }\n\n // Then check DB-stored keys via settings service\n if (_settingsService == null)\n {\n return false;\n }\n\n try\n {\n var apiKey = await _settingsService.GetProviderApiKeyAsync(provider);\n if (!string.IsNullOrWhiteSpace(apiKey))\n {\n return true;\n }\n\n // For local/ollama, check if base URL is configured\n if (provider.ToLower() is \"local\" or \"ollama\")\n {\n var baseUrl = await _settingsService.GetProviderBaseUrlAsync(provider);\n return !string.IsNullOrWhiteSpace(baseUrl);\n }\n }\n catch (Exception ex)\n {\n _logger.LogDebug(ex, \"Error checking DB config for provider {Provider}\", provider);\n }\n\n return false;\n }\n\n /// \n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// \n private async Task ResolveApiKeyAsync(string provider, string configFallback)\n {\n if (_settingsService != null)\n {\n try\n {\n var key = await _settingsService.GetProviderApiKeyAsync(provider);\n if (!string.IsNullOrWhiteSpace(key))\n {\n return key;\n }\n }\n catch (Exception ex)\n {\n _logger.LogWarning(ex, \"Failed to resolve API key from DB for {Provider}, using config\", provider);\n }\n }\n return configFallback;\n }\n\n /// \n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// \n private async Task ResolveBaseUrlAsync(string provider, string configFallback)\n {\n if (_settingsService != null)\n {\n try\n {\n var url = await _settingsService.GetProviderBaseUrlAsync(provider);\n if (!string.IsNullOrWhiteSpace(url))\n {\n return url;\n }\n }\n catch (Exception ex)\n {\n _logger.LogWarning(ex, \"Failed to resolve base URL from DB for {Provider}, using config\", provider);\n }\n }\n return configFallback;\n }\n\n /// \n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// \n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n {\n return false;\n }\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n {\n return false;\n }\n\n return true;\n }\n\n public List GetAvailableProviders()\n {\n var providers = new List\n {\n new() { Value = \"openai\", Label = \"OpenAI\", IsConfigured = IsConfigured(\"openai\"), Models = GetModelsForProvider(\"openai\") },\n new() { Value = \"azure\", Label = \"Azure OpenAI\", IsConfigured = IsConfigured(\"azure\"), Models = GetModelsForProvider(\"azure\") },\n new() { Value = \"anthropic\", Label = \"Anthropic (Claude)\", IsConfigured = IsConfigured(\"anthropic\"), Models = GetModelsForProvider(\"anthropic\") },\n new() { Value = \"google\", Label = \"Google Cloud (Gemini)\", IsConfigured = IsConfigured(\"google\"), Models = GetModelsForProvider(\"google\") },\n new() { Value = \"bedrock\", Label = \"AWS Bedrock\", IsConfigured = IsConfigured(\"bedrock\"), Models = GetModelsForProvider(\"bedrock\") },\n new() { Value = \"deepseek\", Label = \"DeepSeek\", IsConfigured = IsConfigured(\"deepseek\"), Models = GetModelsForProvider(\"deepseek\") },\n new() { Value = \"groq\", Label = \"Groq (Fast Inference)\", IsConfigured = IsConfigured(\"groq\"), Models = GetModelsForProvider(\"groq\") },\n new() { Value = \"allenai\", Label = \"Allen AI (Open Research)\", IsConfigured = IsConfigured(\"allenai\"), Models = GetModelsForProvider(\"allenai\") },\n new() { Value = \"local\", Label = \"Local LLM (Ollama/LM Studio)\", IsConfigured = IsConfigured(\"local\"), Models = GetModelsForProvider(\"local\") },\n new() { Value = \"custom\", Label = \"Custom Endpoint\", IsConfigured = IsConfigured(\"custom\"), Models = GetModelsForProvider(\"custom\") }\n };\n return providers;\n }\n\n public List GetAvailableModels()\n {\n var models = new List();\n foreach (var provider in GetAvailableProviders().Where(p => p.IsConfigured))\n {\n models.AddRange(provider.Models);\n }\n return models;\n }\n\n private List GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List\n {\n new() { Value = \"gpt-4o\", Label = \"GPT-4o\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"gpt-4o\" },\n new() { Value = \"gpt-4o-mini\", Label = \"GPT-4o Mini\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"gpt-4o-mini\" },\n new() { Value = \"gpt-4-turbo\", Label = \"GPT-4 Turbo\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"gpt-4-turbo\" },\n new() { Value = \"gpt-4\", Label = \"GPT-4\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"gpt-4\" },\n new() { Value = \"gpt-3.5-turbo\", Label = \"GPT-3.5 Turbo\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"gpt-3.5-turbo\" },\n new() { Value = \"o1-preview\", Label = \"o1 Preview (Reasoning)\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"o1-preview\" },\n new() { Value = \"o1-mini\", Label = \"o1 Mini\", Provider = \"openai\", IsDefault = _options.OpenAI.DefaultModel == \"o1-mini\" }\n },\n \"azure\" => new List\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List\n {\n new() { Value = \"claude-3-5-sonnet-20241022\", Label = \"Claude 3.5 Sonnet\", Provider = \"anthropic\", IsDefault = _options.Anthropic.DefaultModel?.Contains(\"sonnet\") == true },\n new() { Value = \"claude-3-5-haiku-20241022\", Label = \"Claude 3.5 Haiku\", Provider = \"anthropic\", IsDefault = _options.Anthropic.DefaultModel?.Contains(\"haiku\") == true },\n new() { Value = \"claude-3-opus-20240229\", Label = \"Claude 3 Opus\", Provider = \"anthropic\", IsDefault = _options.Anthropic.DefaultModel?.Contains(\"opus\") == true }\n },\n \"google\" => new List\n {\n new() { Value = \"gemini-2.0-flash-exp\", Label = \"Gemini 2.0 Flash\", Provider = \"google\", IsDefault = _options.GoogleCloud.DefaultModel?.Contains(\"2.0\") == true },\n new() { Value = \"gemini-1.5-pro\", Label = \"Gemini 1.5 Pro\", Provider = \"google\", IsDefault = _options.GoogleCloud.DefaultModel == \"gemini-1.5-pro\" },\n new() { Value = \"gemini-1.5-flash\", Label = \"Gemini 1.5 Flash\", Provider = \"google\", IsDefault = _options.GoogleCloud.DefaultModel == \"gemini-1.5-flash\" }\n },\n \"bedrock\" => new List\n {\n new() { Value = \"anthropic.claude-3-5-sonnet-20241022-v2:0\", Label = \"Claude 3.5 Sonnet (Bedrock)\", Provider = \"bedrock\", IsDefault = _options.AWSBedrock.DefaultModel?.Contains(\"sonnet\") == true },\n new() { Value = \"anthropic.claude-3-haiku-20240307-v1:0\", Label = \"Claude 3 Haiku (Bedrock)\", Provider = \"bedrock\", IsDefault = _options.AWSBedrock.DefaultModel?.Contains(\"haiku\") == true },\n new() { Value = \"amazon.titan-text-premier-v1:0\", Label = \"Amazon Titan Text Premier\", Provider = \"bedrock\", IsDefault = _options.AWSBedrock.DefaultModel?.Contains(\"titan\") == true },\n new() { Value = \"meta.llama3-70b-instruct-v1:0\", Label = \"Llama 3 70B (Bedrock)\", Provider = \"bedrock\", IsDefault = _options.AWSBedrock.DefaultModel?.Contains(\"llama\") == true }\n },\n \"deepseek\" => new List\n {\n new() { Value = \"deepseek-chat\", Label = \"DeepSeek Chat\", Provider = \"deepseek\", IsDefault = _options.DeepSeek.DefaultModel == \"deepseek-chat\" },\n new() { Value = \"deepseek-coder\", Label = \"DeepSeek Coder\", Provider = \"deepseek\", IsDefault = _options.DeepSeek.DefaultModel == \"deepseek-coder\" },\n new() { Value = \"deepseek-reasoner\", Label = \"DeepSeek Reasoner\", Provider = \"deepseek\", IsDefault = _options.DeepSeek.DefaultModel == \"deepseek-reasoner\" }\n },\n \"groq\" => new List\n {\n new() { Value = \"llama-3.3-70b-versatile\", Label = \"Llama 3.3 70B Versatile\", Provider = \"groq\", IsDefault = _options.Groq.DefaultModel == \"llama-3.3-70b-versatile\" },\n new() { Value = \"llama-3.1-8b-instant\", Label = \"Llama 3.1 8B Instant\", Provider = \"groq\", IsDefault = _options.Groq.DefaultModel == \"llama-3.1-8b-instant\" },\n new() { Value = \"mixtral-8x7b-32768\", Label = \"Mixtral 8x7B\", Provider = \"groq\", IsDefault = _options.Groq.DefaultModel == \"mixtral-8x7b-32768\" },\n new() { Value = \"gemma2-9b-it\", Label = \"Gemma 2 9B IT\", Provider = \"groq\", IsDefault = _options.Groq.DefaultModel == \"gemma2-9b-it\" }\n },\n \"allenai\" or \"huggingface\" or \"ai2\" => new List\n {\n new() { Value = \"allenai/OLMo-7B-Instruct\", Label = \"OLMo 7B Instruct\", Provider = \"allenai\", IsDefault = _options.AllenAI.DefaultModel?.Contains(\"OLMo\") == true },\n new() { Value = \"allenai/OLMo-7B\", Label = \"OLMo 7B Base\", Provider = \"allenai\", IsDefault = false },\n new() { Value = \"allenai/tulu-2-7b\", Label = \"Tulu 2 7B\", Provider = \"allenai\", IsDefault = _options.AllenAI.DefaultModel?.Contains(\"tulu\") == true },\n new() { Value = \"allenai/tulu-2-dpo-7b\", Label = \"Tulu 2 DPO 7B\", Provider = \"allenai\", IsDefault = false },\n new() { Value = \"allenai/tulu-2-13b\", Label = \"Tulu 2 13B\", Provider = \"allenai\", IsDefault = false },\n new() { Value = \"allenai/OLMo-1B\", Label = \"OLMo 1B (Fast)\", Provider = \"allenai\", IsDefault = false }\n },\n \"local\" => new List\n {\n new() { Value = _options.LocalLLM.DefaultModel, Label = $\"Local: {_options.LocalLLM.DefaultModel}\", Provider = \"local\", IsDefault = true },\n new() { Value = \"llama3\", Label = \"Llama 3\", Provider = \"local\" },\n new() { Value = \"mistral\", Label = \"Mistral\", Provider = \"local\" },\n new() { Value = \"codellama\", Label = \"Code Llama\", Provider = \"local\" },\n new() { Value = \"phi3\", Label = \"Phi-3\", Provider = \"local\" }\n },\n \"custom\" => new List\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List()\n };\n }\n\n public async Task CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List\n {\n new() { Role = \"user\", Content = InterpolateVariables(request.Prompt, request.Variables) }\n };\n\n if (!string.IsNullOrEmpty(request.SystemPrompt))\n {\n request.Messages.Insert(0, new LLMMessage { Role = \"system\", Content = request.SystemPrompt });\n }\n\n return await ChatAsync(request, cancellationToken);\n }\n\n public async Task ChatAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Use resilience service if available for better error handling and circuit breaking\n if (_resilienceService != null)\n {\n return await _resilienceService.ExecuteWithFallbackAsync(\n $\"LLM-{request.Provider}\",\n async ct => await ExecuteChatInternalAsync(request, ct),\n ex => new LLMResponse\n {\n Success = false,\n Error = $\"LLM service temporarily unavailable: {ex.Message}\",\n Provider = request.Provider\n },\n cancellationToken);\n }\n\n return await ExecuteChatInternalAsync(request, cancellationToken);\n }\n\n private async Task ExecuteChatInternalAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var startTime = DateTime.UtcNow;\n var providers = GetProviderOrder(request.Provider);\n Exception? lastException = null;\n\n foreach (var provider in providers)\n {\n var isReady = await IsConfiguredAsync(provider);\n if (!isReady)\n {\n _logger.LogDebug(\"Skipping provider {Provider} - not configured\", provider);\n continue;\n }\n\n try\n {\n var response = provider.ToLower() switch\n {\n // AP-036: Delegated to OpenAILLMProvider (original CallOpenAIAsync preserved below)\n \"openai\" => await _openAIProvider.CallAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n // AP-036: Delegated to AnthropicLLMProvider (original CallAnthropicAsync preserved below)\n \"anthropic\" => await _anthropicProvider.CallAsync(request, cancellationToken),\n \"google\" or \"gemini\" or \"vertexai\" => await CallGoogleCloudAsync(request, cancellationToken),\n \"aws\" or \"bedrock\" => await CallAWSBedrockAsync(request, cancellationToken),\n \"deepseek\" => await CallDeepSeekAsync(request, cancellationToken),\n \"groq\" => await CallGroqAsync(request, cancellationToken),\n \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n // AP-036: Delegated to LocalLLMProvider (original CallLocalLLMAsync preserved below)\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await _localProvider.CallAsync(request, cancellationToken),\n \"custom\" => await CallCustomEndpointAsync(request, cancellationToken),\n _ => throw new NotSupportedException($\"Provider {provider} is not supported\")\n };\n\n response.DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds;\n\n // Parse JSON if requested\n if (request.JsonMode && response.Success)\n {\n try\n {\n response.ParsedJson = JsonSerializer.Deserialize