Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
1.1 MiB
Plaintext
1 line
1.1 MiB
Plaintext
{"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/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _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<LLMService> logger,\n IOptions<LLMProviderOptions> 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<OpenAILLMProvider>.Instance);\n _anthropicProvider = new AnthropicLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<AnthropicLLMProvider>.Instance);\n _localProvider = new LocalLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<LocalLLMProvider>.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 /// <summary>\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 /// </summary>\n public async Task<bool> 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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\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<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n {\n break;\n }\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n {\n return template;\n }\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n {\n return \"\";\n }\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n {\n promptTokens = pec.GetInt32();\n }\n if (root.TryGetProperty(\"eval_count\", out var ec))\n {\n completionTokens = ec.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n {\n promptTokens = pt.GetInt32();\n }\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n {\n completionTokens = ct.GetInt32();\n }\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "f0bf8f798e5d96ee3b6915fdd66e27fa62a69799": {"t": 1785760513.6655896, "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/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _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<LLMService> logger,\n IOptions<LLMProviderOptions> 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<OpenAILLMProvider>.Instance);\n _anthropicProvider = new AnthropicLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<AnthropicLLMProvider>.Instance);\n _localProvider = new LocalLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<LocalLLMProvider>.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 /// <summary>\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 /// </summary>\n public async Task<bool> 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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\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<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n {\n break;\n }\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n {\n return template;\n }\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n {\n return \"\";\n }\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n {\n promptTokens = pec.GetInt32();\n }\n if (root.TryGetProperty(\"eval_count\", out var ec))\n {\n completionTokens = ec.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n {\n promptTokens = pt.GetInt32();\n }\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n {\n completionTokens = ct.GetInt32();\n }\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "944c77d48fcb042bcf2b96967a9ec0f2774a8f26": {"t": 1785760515.3522835, "n": 65710, "h": "75a0606a73d8cae4", "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 Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n private readonly ILLMSettingsService? _settingsService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> 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\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 /// <summary>\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 /// </summary>\n public async Task<bool> 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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\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<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n {\n break;\n }\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n {\n return template;\n }\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n {\n return \"\";\n }\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n {\n promptTokens = pec.GetInt32();\n }\n if (root.TryGetProperty(\"eval_count\", out var ec))\n {\n completionTokens = ec.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n {\n promptTokens = pt.GetInt32();\n }\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n {\n completionTokens = ct.GetInt32();\n }\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "4b579ffccff33ea53e8638a2652c19b69f2ce6ca": {"t": 1785760516.9693391, "n": 65607, "h": "35ec3b5a19a7fab1", "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 Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n private readonly ILLMSettingsService? _settingsService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> 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\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 /// <summary>\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 /// </summary>\n public async Task<bool> 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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\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<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n {\n break;\n }\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n {\n return template;\n }\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n {\n return \"\";\n }\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n {\n promptTokens = pec.GetInt32();\n }\n if (root.TryGetProperty(\"eval_count\", out var ec))\n {\n completionTokens = ec.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n {\n promptTokens = pt.GetInt32();\n }\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n {\n completionTokens = ct.GetInt32();\n }\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "7f27f8b6276cd2f3c7aacc0572b3a08a62cfd06e": {"t": 1785760518.6187987, "n": 65255, "h": "2ac5f5d44bde5661", "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 Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n private readonly ILLMSettingsService? _settingsService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> 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\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 /// <summary>\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 /// </summary>\n public async Task<bool> IsConfiguredAsync(string provider)\n {\n // First check config (fast path)\n if (IsConfigured(provider))\n return true;\n\n // Then check DB-stored keys via settings service\n if (_settingsService == null)\n return false;\n\n try\n {\n var apiKey = await _settingsService.GetProviderApiKeyAsync(provider);\n if (!string.IsNullOrWhiteSpace(apiKey))\n return true;\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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 return key;\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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 return url;\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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "420771bb73bae6d148f612e1f5da33b8dde2bfd7": {"t": 1785760520.163194, "n": 65222, "h": "19288086651bfc89", "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 Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n private readonly ILLMSettingsService? _settingsService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> 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\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 /// <summary>\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 /// </summary>\n public async Task<bool> IsConfiguredAsync(string provider)\n {\n // First check config (fast path)\n if (IsConfigured(provider))\n return true;\n\n // Then check DB-stored keys via settings service\n if (_settingsService == null)\n return false;\n\n try\n {\n var apiKey = await _settingsService.GetProviderApiKeyAsync(provider);\n if (!string.IsNullOrWhiteSpace(apiKey))\n return true;\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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 return key;\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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 return url;\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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "87ed4d6b53ee6c0596d6754bc7793340f7fce644": {"t": 1785760522.2623968, "n": 61358, "h": "99921b1d55355ce8", "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 Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.Groq.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "5ab6a00c448834db623dd31001a56768609ee28c": {"t": 1785760524.8268106, "n": 61327, "h": "aec7e0b181253f8d", "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.\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.Groq.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "8f8a712966d910f93c47ec3edde31f4ca16197a6": {"t": 1785760526.4009278, "n": 61777, "h": "12e91d94018cdfb3", "c": "// CRM Solution - Customer Relationship Management System\n// Copyright (C) 2024-2026 Abhishek Lal\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.Groq.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "c19c7cbcd093235ddc5b99f1deea8b8c30bc1d48": {"t": 1785760529.1932595, "n": 58020, "h": "d72d5c6307b18672", "c": "// CRM Solution - Customer Relationship Management System\n// Copyright (C) 2024-2026 Abhishek Lal\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 \"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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "5d46ed0e00434823ede03d5dec233debd8e0d68f": {"t": 1785760530.6729531, "n": 58324, "h": "ba34755f098c23c1", "c": "// CRM Solution - Customer Relationship Management System\n// Copyright (C) 2024-2026 Abhishek Lal\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\n\n/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 \"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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n\n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n\n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "865283f32df2d04b189ed26bdaf313b235e9b9b7": {"t": 1785760532.3456264, "n": 57751, "h": "9a5cce50f4587c62", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n \n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey \n { \n get => _apiKey; \n set => _apiKey = value; \n }\n \n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\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 \"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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\n private static bool IsValidApiKey(string? value)\n {\n if (string.IsNullOrWhiteSpace(value))\n return false;\n \n // Check for unresolved environment variable placeholders like ${VAR:} or ${VAR:default}\n if (value.StartsWith(\"${\") && value.Contains(\":\"))\n return false;\n \n return true;\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n \n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n \n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) && \n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n \n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n \n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n \n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n \n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "f2fa20608d94c4204056f8b6b1d4249e946dd8c2": {"t": 1785760534.1064003, "n": 57356, "h": "5be9feb289776333", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n \n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey \n { \n get => _apiKey; \n set => _apiKey = value; \n }\n \n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => !string.IsNullOrEmpty(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => !string.IsNullOrEmpty(_options.AzureOpenAI.Endpoint) && \n !string.IsNullOrEmpty(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => !string.IsNullOrEmpty(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => !string.IsNullOrEmpty(_options.GoogleCloud.ApiKey) || \n !string.IsNullOrEmpty(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials || \n (!string.IsNullOrEmpty(_options.AWSBedrock.AccessKeyId) && \n !string.IsNullOrEmpty(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => !string.IsNullOrEmpty(_options.DeepSeek.ApiKey),\n \"allenai\" or \"huggingface\" or \"ai2\" => _options.AllenAI.Enabled && \n !string.IsNullOrEmpty(_options.AllenAI.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled && \n !string.IsNullOrEmpty(_options.LocalLLM.BaseUrl),\n \"custom\" => !string.IsNullOrEmpty(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n \n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n \n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) && \n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n \n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n \n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n \n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n \n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "3b6579d45f33e3c7c3411f08e69d3030d07e4ba0": {"t": 1785760535.5574996, "n": 57590, "h": "e8a6c019477d86a7", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n \n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey \n { \n get => _apiKey; \n set => _apiKey = value; \n }\n \n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\n public string ApiKey { get; set; } = \"\"; // Optional for some local servers\n public string DefaultModel { get; set; } = \"olmo2:7b\"; // Allen AI OLMo2\n public string FallbackModel { get; set; } = \"llama3:8b\"; // Fallback model\n public string ApiFormat { get; set; } = \"ollama\"; // ollama, openai, or custom\n public Dictionary<string, string> Headers { get; set; } = new();\n public bool Enabled { get; set; } = true;\n public int TimeoutSeconds { get; set; } = 120; // Longer timeout for local models\n public int MaxTokens { get; set; } = 2048;\n}\n\npublic class CustomEndpointOptions\n{\n public string Url { get; set; } = \"\";\n public string ApiKey { get; set; } = \"\";\n public Dictionary<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => !string.IsNullOrEmpty(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => !string.IsNullOrEmpty(_options.AzureOpenAI.Endpoint) && \n !string.IsNullOrEmpty(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => !string.IsNullOrEmpty(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => !string.IsNullOrEmpty(_options.GoogleCloud.ApiKey) || \n !string.IsNullOrEmpty(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials || \n (!string.IsNullOrEmpty(_options.AWSBedrock.AccessKeyId) && \n !string.IsNullOrEmpty(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => !string.IsNullOrEmpty(_options.DeepSeek.ApiKey),\n \"allenai\" or \"huggingface\" or \"ai2\" => _options.AllenAI.Enabled && \n !string.IsNullOrEmpty(_options.AllenAI.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled && \n !string.IsNullOrEmpty(_options.LocalLLM.BaseUrl),\n \"custom\" => !string.IsNullOrEmpty(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n \n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n \n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) && \n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n \n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n \n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n \n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n \n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "60b6788786412d85c5ebb83125c46e017f8b7337": {"t": 1785760537.06312, "n": 56495, "h": "1c7c8fa67a15da86", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n public string ApiKey { get; set; } = \"\"; // Hugging Face API token\n public string BaseUrl { get; set; } = \"https://api-inference.huggingface.co/models\";\n public string DefaultModel { get; set; } = \"allenai/OLMo-7B-Instruct\"; // or allenai/tulu-2-7b\n public bool Enabled { get; set; } = true;\n public int MaxNewTokens { get; set; } = 1000;\n public double Temperature { get; set; } = 0.7;\n}\n\n/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => !string.IsNullOrEmpty(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => !string.IsNullOrEmpty(_options.AzureOpenAI.Endpoint) && \n !string.IsNullOrEmpty(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => !string.IsNullOrEmpty(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => !string.IsNullOrEmpty(_options.GoogleCloud.ApiKey) || \n !string.IsNullOrEmpty(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials || \n (!string.IsNullOrEmpty(_options.AWSBedrock.AccessKeyId) && \n !string.IsNullOrEmpty(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => !string.IsNullOrEmpty(_options.DeepSeek.ApiKey),\n \"allenai\" or \"huggingface\" or \"ai2\" => _options.AllenAI.Enabled && \n !string.IsNullOrEmpty(_options.AllenAI.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled && \n !string.IsNullOrEmpty(_options.LocalLLM.BaseUrl),\n \"custom\" => !string.IsNullOrEmpty(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"allenai\" or \"huggingface\" or \"ai2\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"allenai\" or \"huggingface\" or \"ai2\" => await CallAllenAIAsync(request, cancellationToken),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n \n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.AllenAI.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n \n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) && \n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n \n throw new Exception($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n \n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n return \"\";\n\n var promptBuilder = new StringBuilder();\n \n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n \n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "0b5cc917a71b700c16f9a350f07b87ce9feb3764": {"t": 1785760538.6314151, "n": 49649, "h": "0a3be383e3679788", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n private readonly IResilienceService? _resilienceService;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null,\n IResilienceService? resilienceService = 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 }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => !string.IsNullOrEmpty(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => !string.IsNullOrEmpty(_options.AzureOpenAI.Endpoint) && \n !string.IsNullOrEmpty(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => !string.IsNullOrEmpty(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => !string.IsNullOrEmpty(_options.GoogleCloud.ApiKey) || \n !string.IsNullOrEmpty(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials || \n (!string.IsNullOrEmpty(_options.AWSBedrock.AccessKeyId) && \n !string.IsNullOrEmpty(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => !string.IsNullOrEmpty(_options.DeepSeek.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled && \n !string.IsNullOrEmpty(_options.LocalLLM.BaseUrl),\n \"custom\" => !string.IsNullOrEmpty(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"local\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "ce4b79cd0a7ac2d8c5a66f09821480b262c7dd40": {"t": 1785760540.2091422, "n": 48661, "h": "896938887abac34c", "c": "/**\n * CRM Solution - Customer Relationship Management System\n * Copyright (C) 2024-2026 Abhishek Lal\n *\n * LLM Integration Service - Multi-provider AI service with fallback support\n */\n\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace CRM.Infrastructure.Services;\n\n/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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 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\", \"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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\npublic class LocalLLMOptions\n{\n public string BaseUrl { get; set; } = \"http://localhost:11434\"; // Ollama default\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _logger;\n private readonly LLMProviderOptions _options;\n private readonly HttpClient _httpClient;\n\n public LLMService(\n ILogger<LLMService> logger,\n IOptions<LLMProviderOptions> options,\n HttpClient? httpClient = null)\n {\n _logger = logger;\n _options = options.Value;\n _httpClient = httpClient ?? new HttpClient();\n _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);\n }\n\n public bool IsConfigured(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => !string.IsNullOrEmpty(_options.OpenAI.ApiKey),\n \"azure\" or \"azureopenai\" => !string.IsNullOrEmpty(_options.AzureOpenAI.Endpoint) && \n !string.IsNullOrEmpty(_options.AzureOpenAI.ApiKey),\n \"anthropic\" => !string.IsNullOrEmpty(_options.Anthropic.ApiKey),\n \"google\" or \"gemini\" or \"vertexai\" => !string.IsNullOrEmpty(_options.GoogleCloud.ApiKey) || \n !string.IsNullOrEmpty(_options.GoogleCloud.ServiceAccountKeyPath),\n \"aws\" or \"bedrock\" => _options.AWSBedrock.UseDefaultCredentials || \n (!string.IsNullOrEmpty(_options.AWSBedrock.AccessKeyId) && \n !string.IsNullOrEmpty(_options.AWSBedrock.SecretAccessKey)),\n \"deepseek\" => !string.IsNullOrEmpty(_options.DeepSeek.ApiKey),\n \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => _options.LocalLLM.Enabled && \n !string.IsNullOrEmpty(_options.LocalLLM.BaseUrl),\n \"custom\" => !string.IsNullOrEmpty(_options.CustomEndpoint.Url),\n _ => false\n };\n }\n\n public List<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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 = \"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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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 \"local\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default)\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 if (!IsConfigured(provider))\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 \"openai\" => await CallOpenAIAsync(request, cancellationToken),\n \"azure\" or \"azureopenai\" => await CallAzureOpenAIAsync(request, cancellationToken),\n \"anthropic\" => await CallAnthropicAsync(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 \"local\" or \"ollama\" or \"lmstudio\" or \"vllm\" => await CallLocalLLMAsync(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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n \n if (!_options.EnableFallback) break;\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0) return template;\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.OpenAI.BaseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.OpenAI.ApiKey);\n \n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{_options.AzureOpenAI.Endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"api-key\", _options.AzureOpenAI.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Anthropic.BaseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n httpRequest.Headers.Add(\"x-api-key\", _options.Anthropic.ApiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n \n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Gemini API format\n var contents = new List<object>();\n \n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={_options.GoogleCloud.ApiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.GoogleCloud.ApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n \n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", _options.AWSBedrock.AccessKeyId);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n \n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {_options.DeepSeek.ApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new Exception($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n promptTokens = pec.GetInt32();\n if (root.TryGetProperty(\"eval_count\", out var ec))\n completionTokens = ec.GetInt32();\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n \n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n promptTokens = pt.GetInt32();\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n completionTokens = ct.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\"\n )\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.CustomEndpoint.ApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}, "0160ae66fd69727911d2870d42b124a59016880c": {"t": 1786068388.115617, "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/// <summary>\n/// Configuration for LLM providers\n/// </summary>\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/// <summary>\n/// Azure OpenAI Service configuration\n/// </summary>\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/// <summary>\n/// Google Cloud Vertex AI / Gemini configuration\n/// </summary>\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/// <summary>\n/// AWS Bedrock configuration\n/// </summary>\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/// <summary>\n/// DeepSeek AI configuration\n/// </summary>\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/// <summary>\n/// Groq AI configuration - Ultra-fast inference with OpenAI-compatible API\n/// Models: Llama 3.3, Mixtral, Gemma via custom LPU hardware\n/// </summary>\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/// <summary>\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/// </summary>\npublic class AllenAIOptions\n{\n private string _apiKey = \"\";\n\n /// <summary>Hugging Face API token (alias: HuggingFaceApiKey)</summary>\n public string ApiKey\n {\n get => _apiKey;\n set => _apiKey = value;\n }\n\n /// <summary>Hugging Face API token (alias for ApiKey, for backwards compatibility)</summary>\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/// <summary>\n/// Local/Self-hosted LLM configuration (Ollama, LM Studio, vLLM, etc.)\n/// </summary>\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<string, string> 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<string, string> Headers { get; set; } = new();\n}\n\n/// <summary>\n/// LLM request parameters\n/// </summary>\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<LLMMessage>? 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<string, object>? 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/// <summary>\n/// LLM response\n/// </summary>\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/// <summary>\n/// LLM provider info for frontend\n/// </summary>\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<LLMModelInfo> Models { get; set; } = new();\n}\n\n/// <summary>\n/// LLM model info for frontend\n/// </summary>\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/// <summary>\n/// Interface for LLM service\n/// </summary>\npublic interface ILLMService\n{\n Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default);\n Task<LLMResponse> ChatAsync(LLMRequest request, CancellationToken cancellationToken = default);\n bool IsConfigured(string provider);\n Task<bool> IsConfiguredAsync(string provider);\n List<LLMProviderInfo> GetAvailableProviders();\n List<LLMModelInfo> GetAvailableModels();\n}\n\n/// <summary>\n/// Multi-provider LLM service with fallback support\n/// </summary>\npublic class LLMService : ILLMService\n{\n private readonly ILogger<LLMService> _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<LLMService> logger,\n IOptions<LLMProviderOptions> 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<OpenAILLMProvider>.Instance);\n _anthropicProvider = new AnthropicLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<AnthropicLLMProvider>.Instance);\n _localProvider = new LocalLLMProvider(_options, _httpClient, _settingsService, Microsoft.Extensions.Logging.Abstractions.NullLogger<LocalLLMProvider>.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 /// <summary>\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 /// </summary>\n public async Task<bool> 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 /// <summary>\n /// Resolves the API key for a provider, checking DB-stored encrypted keys first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Resolves the base URL for a provider, checking DB first, then falling back to config.\n /// </summary>\n private async Task<string> 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 /// <summary>\n /// Checks if an API key or URL is valid (not empty and not an unresolved placeholder)\n /// </summary>\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<LLMProviderInfo> GetAvailableProviders()\n {\n var providers = new List<LLMProviderInfo>\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<LLMModelInfo> GetAvailableModels()\n {\n var models = new List<LLMModelInfo>();\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<LLMModelInfo> GetModelsForProvider(string provider)\n {\n return provider.ToLower() switch\n {\n \"openai\" => new List<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = _options.AzureOpenAI.DeploymentName, Label = $\"Azure: {_options.AzureOpenAI.DeploymentName}\", Provider = \"azure\", IsDefault = true }\n },\n \"anthropic\" => new List<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\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<LLMModelInfo>\n {\n new() { Value = \"custom\", Label = \"Custom Model\", Provider = \"custom\", IsDefault = true }\n },\n _ => new List<LLMModelInfo>()\n };\n }\n\n public async Task<LLMResponse> CompletionAsync(LLMRequest request, CancellationToken cancellationToken = default)\n {\n // Convert to chat format for modern APIs\n request.Messages = new List<LLMMessage>\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<LLMResponse> 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<LLMResponse> 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<object>(response.Content);\n }\n catch (JsonException ex)\n {\n _logger.LogWarning(ex, \"Failed to parse LLM response as JSON\");\n }\n }\n\n return response;\n }\n catch (Exception ex)\n {\n lastException = ex;\n _logger.LogWarning(ex, \"LLM call to {Provider} failed, trying fallback\", provider);\n\n if (!_options.EnableFallback)\n {\n break;\n }\n }\n }\n\n return new LLMResponse\n {\n Success = false,\n Error = lastException?.Message ?? \"No LLM provider available\",\n DurationMs = (DateTime.UtcNow - startTime).TotalMilliseconds\n };\n }\n\n private string[] GetProviderOrder(string requestedProvider)\n {\n if (!_options.EnableFallback)\n {\n return new[] { requestedProvider };\n }\n\n var order = new List<string> { requestedProvider };\n foreach (var fallback in _options.FallbackOrder)\n {\n if (fallback != requestedProvider)\n {\n order.Add(fallback);\n }\n }\n return order.ToArray();\n }\n\n private string InterpolateVariables(string template, Dictionary<string, object>? variables)\n {\n if (variables == null || variables.Count == 0)\n {\n return template;\n }\n\n var result = template;\n foreach (var kvp in variables)\n {\n result = result.Replace($\"{{{{{kvp.Key}}}}}\", kvp.Value?.ToString() ?? \"\");\n }\n return result;\n }\n\n #region OpenAI Provider\n\n private async Task<LLMResponse> CallOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gpt\") ? request.Model : _options.OpenAI.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"openai\", _options.OpenAI.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"openai\", _options.OpenAI.BaseUrl);\n\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/chat/completions\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n\n if (!string.IsNullOrEmpty(_options.OpenAI.Organization))\n {\n httpRequest.Headers.Add(\"OpenAI-Organization\", _options.OpenAI.Organization);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"openai\",\n Model = model,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Azure OpenAI Provider\n\n private async Task<LLMResponse> CallAzureOpenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var deploymentName = _options.AzureOpenAI.DeploymentName;\n var apiVersion = _options.AzureOpenAI.ApiVersion;\n var apiKey = await ResolveApiKeyAsync(\"azure\", _options.AzureOpenAI.ApiKey);\n var endpoint = await ResolveBaseUrlAsync(\"azure\", _options.AzureOpenAI.Endpoint);\n\n var requestBody = new\n {\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n response_format = request.JsonMode ? new { type = \"json_object\" } : null\n };\n\n var url = $\"{endpoint}/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"api-key\", apiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Azure OpenAI API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Azure OpenAI API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"azure\",\n Model = _options.AzureOpenAI.DefaultModel,\n Content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\",\n PromptTokens = root.GetProperty(\"usage\").GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = root.GetProperty(\"usage\").GetProperty(\"completion_tokens\").GetInt32(),\n TotalTokens = root.GetProperty(\"usage\").GetProperty(\"total_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Anthropic Provider\n\n private async Task<LLMResponse> CallAnthropicAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"claude\") ? request.Model : _options.Anthropic.DefaultModel;\n var apiKey = await ResolveApiKeyAsync(\"anthropic\", _options.Anthropic.ApiKey);\n var baseUrl = await ResolveBaseUrlAsync(\"anthropic\", _options.Anthropic.BaseUrl);\n\n // Extract system message\n string? systemMessage = null;\n var messages = request.Messages?.ToList() ?? new List<LLMMessage>();\n var systemMsg = messages.FirstOrDefault(m => m.Role == \"system\");\n if (systemMsg != null)\n {\n systemMessage = systemMsg.Content;\n messages.Remove(systemMsg);\n }\n\n var requestBody = new\n {\n model,\n system = systemMessage,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{baseUrl}/messages\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n httpRequest.Headers.Add(\"x-api-key\", apiKey);\n httpRequest.Headers.Add(\"anthropic-version\", _options.Anthropic.ApiVersion);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Anthropic API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Anthropic API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n var usage = root.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"anthropic\",\n Model = model,\n Content = content,\n PromptTokens = usage.GetProperty(\"input_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"output_tokens\").GetInt32(),\n TotalTokens = usage.GetProperty(\"input_tokens\").GetInt32() + usage.GetProperty(\"output_tokens\").GetInt32()\n };\n }\n\n #endregion\n\n #region Google Cloud (Gemini / Vertex AI)\n\n private async Task<LLMResponse> CallGoogleCloudAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.StartsWith(\"gemini\") ? request.Model : _options.GoogleCloud.DefaultModel;\n\n if (_options.GoogleCloud.UseVertexAI)\n {\n return await CallVertexAIAsync(request, model, cancellationToken);\n }\n else\n {\n return await CallGeminiAPIAsync(request, model, cancellationToken);\n }\n }\n\n private async Task<LLMResponse> CallGeminiAPIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n var apiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n\n // Gemini API format\n var contents = new List<object>();\n\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens,\n responseMimeType = request.JsonMode ? \"application/json\" : \"text/plain\"\n }\n };\n\n var url = $\"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Gemini API error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Gemini API error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n var usageMetadata = root.GetProperty(\"usageMetadata\");\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"google\",\n Model = model,\n Content = contentText,\n PromptTokens = usageMetadata.TryGetProperty(\"promptTokenCount\", out var pt) ? pt.GetInt32() : 0,\n CompletionTokens = usageMetadata.TryGetProperty(\"candidatesTokenCount\", out var ct) ? ct.GetInt32() : 0,\n TotalTokens = usageMetadata.TryGetProperty(\"totalTokenCount\", out var tt) ? tt.GetInt32() : 0\n };\n }\n\n private async Task<LLMResponse> CallVertexAIAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Vertex AI requires OAuth2 token from service account\n // For simplicity, using API key approach - in production, use Google.Cloud.AIPlatform SDK\n var projectId = _options.GoogleCloud.ProjectId;\n var location = _options.GoogleCloud.Location;\n\n var contents = new List<object>();\n foreach (var msg in request.Messages ?? new List<LLMMessage>())\n {\n contents.Add(new\n {\n role = msg.Role == \"assistant\" ? \"model\" : msg.Role,\n parts = new[] { new { text = msg.Content } }\n });\n }\n\n var requestBody = new\n {\n contents,\n generationConfig = new\n {\n temperature = request.Temperature,\n maxOutputTokens = request.MaxTokens\n }\n };\n\n var url = $\"https://{location}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{location}/publishers/google/models/{model}:generateContent\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // Note: In production, use Google.Cloud.AIPlatform.V1 SDK with proper auth\n var vertexApiKey = await ResolveApiKeyAsync(\"google\", _options.GoogleCloud.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", vertexApiKey);\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Vertex AI error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Vertex AI error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var contentText = root.GetProperty(\"candidates\")[0]\n .GetProperty(\"content\")\n .GetProperty(\"parts\")[0]\n .GetProperty(\"text\").GetString() ?? \"\";\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"vertexai\",\n Model = model,\n Content = contentText\n };\n }\n\n #endregion\n\n #region AWS Bedrock\n\n private async Task<LLMResponse> CallAWSBedrockAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = request.Model.Contains(\".\") ? request.Model : _options.AWSBedrock.DefaultModel;\n var region = _options.AWSBedrock.Region;\n\n // Determine the request format based on model provider\n object requestBody;\n if (model.StartsWith(\"anthropic.\"))\n {\n // Anthropic Claude format for Bedrock\n var messages = request.Messages?.Where(m => m.Role != \"system\").ToList() ?? new List<LLMMessage>();\n var systemMsg = request.Messages?.FirstOrDefault(m => m.Role == \"system\");\n\n requestBody = new\n {\n anthropic_version = \"bedrock-2023-05-31\",\n max_tokens = request.MaxTokens,\n system = systemMsg?.Content,\n messages = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature\n };\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n // Amazon Titan format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n inputText = prompt,\n textGenerationConfig = new\n {\n maxTokenCount = request.MaxTokens,\n temperature = request.Temperature\n }\n };\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n // Meta Llama format\n var prompt = string.Join(\"\\n\", request.Messages?.Select(m => $\"{m.Role}: {m.Content}\") ?? Array.Empty<string>());\n requestBody = new\n {\n prompt,\n max_gen_len = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n else\n {\n // Generic format\n requestBody = new\n {\n prompt = string.Join(\"\\n\", request.Messages?.Select(m => m.Content) ?? Array.Empty<string>()),\n max_tokens = request.MaxTokens,\n temperature = request.Temperature\n };\n }\n\n var url = $\"https://bedrock-runtime.{region}.amazonaws.com/model/{model}/invoke\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8,\n \"application/json\")\n };\n\n // AWS Signature V4 signing would be needed here\n // For production, use AWS SDK for .NET (AWSSDK.BedrockRuntime)\n // This is a simplified implementation\n if (!_options.AWSBedrock.UseDefaultCredentials)\n {\n // Add basic auth headers (in production, use proper AWS SigV4)\n var awsAccessKey = await ResolveApiKeyAsync(\"bedrock\", _options.AWSBedrock.AccessKeyId);\n httpRequest.Headers.Add(\"X-Amz-Access-Key\", awsAccessKey);\n if (!string.IsNullOrEmpty(_options.AWSBedrock.SessionToken))\n {\n httpRequest.Headers.Add(\"X-Amz-Security-Token\", _options.AWSBedrock.SessionToken);\n }\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"AWS Bedrock error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"AWS Bedrock error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int inputTokens = 0, outputTokens = 0;\n\n // Parse based on model type\n if (model.StartsWith(\"anthropic.\"))\n {\n content = root.GetProperty(\"content\")[0].GetProperty(\"text\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n inputTokens = usage.GetProperty(\"input_tokens\").GetInt32();\n outputTokens = usage.GetProperty(\"output_tokens\").GetInt32();\n }\n }\n else if (model.StartsWith(\"amazon.titan\"))\n {\n content = root.GetProperty(\"results\")[0].GetProperty(\"outputText\").GetString() ?? \"\";\n }\n else if (model.StartsWith(\"meta.llama\"))\n {\n content = root.GetProperty(\"generation\").GetString() ?? \"\";\n }\n else\n {\n content = root.TryGetProperty(\"completion\", out var comp) ? comp.GetString() ?? \"\" : responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"bedrock\",\n Model = model,\n Content = content,\n PromptTokens = inputTokens,\n CompletionTokens = outputTokens,\n TotalTokens = inputTokens + outputTokens\n };\n }\n\n #endregion\n\n #region DeepSeek\n\n private async Task<LLMResponse> CallDeepSeekAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // DeepSeek uses OpenAI-compatible API\n var model = request.Model?.StartsWith(\"deepseek\") == true ? request.Model : _options.DeepSeek.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.DeepSeek.BaseUrl}/v1/chat/completions\");\n var deepSeekApiKey = await ResolveApiKeyAsync(\"deepseek\", _options.DeepSeek.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {deepSeekApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"DeepSeek API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"DeepSeek API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"deepseek\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Groq Provider\n\n private async Task<LLMResponse> CallGroqAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Groq uses OpenAI-compatible API at https://api.groq.com/openai/v1\n var model = request.Model?.StartsWith(\"llama\") == true || request.Model?.StartsWith(\"mixtral\") == true || request.Model?.StartsWith(\"gemma\") == true\n ? request.Model : _options.Groq.DefaultModel;\n\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n max_tokens = request.MaxTokens > 0 ? request.MaxTokens : 4096,\n temperature = request.Temperature\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.Groq.BaseUrl}/chat/completions\");\n var groqApiKey = await ResolveApiKeyAsync(\"groq\", _options.Groq.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {groqApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Groq API error: {StatusCode} - {Content}\", response.StatusCode, content);\n throw new HttpRequestException($\"Groq API error: {response.StatusCode}\");\n }\n\n var jsonResponse = JsonDocument.Parse(content);\n var responseContent = jsonResponse.RootElement\n .GetProperty(\"choices\")[0]\n .GetProperty(\"message\")\n .GetProperty(\"content\")\n .GetString() ?? \"\";\n\n var usage = jsonResponse.RootElement.GetProperty(\"usage\");\n\n return new LLMResponse\n {\n Content = responseContent,\n Model = model,\n Provider = \"groq\",\n TotalTokens = usage.GetProperty(\"total_tokens\").GetInt32(),\n PromptTokens = usage.GetProperty(\"prompt_tokens\").GetInt32(),\n CompletionTokens = usage.GetProperty(\"completion_tokens\").GetInt32(),\n Success = true\n };\n }\n\n #endregion\n\n #region Allen AI (Hugging Face Inference API)\n\n private async Task<LLMResponse> CallAllenAIAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n // Allen AI models are hosted on Hugging Face\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.AllenAI.DefaultModel;\n\n // Build the prompt from messages (Hugging Face text-generation expects a single prompt)\n var prompt = BuildPromptFromMessages(request.Messages);\n\n var requestBody = new\n {\n inputs = prompt,\n parameters = new\n {\n max_new_tokens = request.MaxTokens > 0 ? request.MaxTokens : _options.AllenAI.MaxNewTokens,\n temperature = request.Temperature > 0 ? request.Temperature : _options.AllenAI.Temperature,\n return_full_text = false,\n do_sample = true\n }\n };\n\n var modelUrl = model.StartsWith(\"http\") ? model : $\"{_options.AllenAI.BaseUrl}/{model}\";\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, modelUrl);\n var allenApiKey = await ResolveApiKeyAsync(\"allenai\", _options.AllenAI.ApiKey);\n httpRequest.Headers.Add(\"Authorization\", $\"Bearer {allenApiKey}\");\n httpRequest.Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\");\n\n var response = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var content = await response.Content.ReadAsStringAsync(cancellationToken);\n\n if (!response.IsSuccessStatusCode)\n {\n _logger.LogError(\"Allen AI/Hugging Face API error: {StatusCode} - {Content}\", response.StatusCode, content);\n\n // Handle model loading (503 when model is cold)\n if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)\n {\n var errorDoc = JsonDocument.Parse(content);\n if (errorDoc.RootElement.TryGetProperty(\"error\", out var error) &&\n error.GetString()?.Contains(\"loading\") == true)\n {\n return new LLMResponse\n {\n Success = false,\n Error = \"Model is loading. Please try again in a few seconds.\",\n Provider = \"allenai\",\n Model = model\n };\n }\n }\n\n throw new HttpRequestException($\"Allen AI API error: {response.StatusCode}\");\n }\n\n // Hugging Face returns an array of generated texts\n var jsonResponse = JsonDocument.Parse(content);\n string responseContent;\n\n if (jsonResponse.RootElement.ValueKind == JsonValueKind.Array)\n {\n responseContent = jsonResponse.RootElement[0]\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n else\n {\n responseContent = jsonResponse.RootElement\n .GetProperty(\"generated_text\")\n .GetString() ?? \"\";\n }\n\n // Estimate tokens (Hugging Face doesn't return token counts for free inference)\n var estimatedPromptTokens = prompt.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n var estimatedCompletionTokens = responseContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;\n\n return new LLMResponse\n {\n Content = responseContent.Trim(),\n Model = model,\n Provider = \"allenai\",\n TotalTokens = estimatedPromptTokens + estimatedCompletionTokens,\n PromptTokens = estimatedPromptTokens,\n CompletionTokens = estimatedCompletionTokens,\n Success = true\n };\n }\n\n private string BuildPromptFromMessages(List<LLMMessage>? messages)\n {\n if (messages == null || messages.Count == 0)\n {\n return \"\";\n }\n\n var promptBuilder = new StringBuilder();\n\n foreach (var message in messages)\n {\n switch (message.Role.ToLower())\n {\n case \"system\":\n promptBuilder.AppendLine($\"<|system|>\\n{message.Content}</s>\");\n break;\n case \"user\":\n promptBuilder.AppendLine($\"<|user|>\\n{message.Content}</s>\");\n break;\n case \"assistant\":\n promptBuilder.AppendLine($\"<|assistant|>\\n{message.Content}</s>\");\n break;\n default:\n promptBuilder.AppendLine(message.Content);\n break;\n }\n }\n\n promptBuilder.AppendLine(\"<|assistant|>\");\n return promptBuilder.ToString();\n }\n\n #endregion\n\n #region Local LLM (Ollama, LM Studio, vLLM)\n\n private async Task<LLMResponse> CallLocalLLMAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var model = !string.IsNullOrEmpty(request.Model) ? request.Model : _options.LocalLLM.DefaultModel;\n var apiFormat = _options.LocalLLM.ApiFormat.ToLower();\n\n return apiFormat switch\n {\n \"ollama\" => await CallOllamaAsync(request, model, cancellationToken),\n \"openai\" => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken),\n _ => await CallLocalOpenAICompatibleAsync(request, model, cancellationToken)\n };\n }\n\n private async Task<LLMResponse> CallOllamaAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // Ollama native API format\n var messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray();\n\n var requestBody = new\n {\n model,\n messages,\n stream = false,\n options = new\n {\n temperature = request.Temperature,\n num_predict = request.MaxTokens\n }\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, $\"{_options.LocalLLM.BaseUrl}/api/chat\")\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Ollama error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Ollama error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"prompt_eval_count\", out var pec))\n {\n promptTokens = pec.GetInt32();\n }\n if (root.TryGetProperty(\"eval_count\", out var ec))\n {\n completionTokens = ec.GetInt32();\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"ollama\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n private async Task<LLMResponse> CallLocalOpenAICompatibleAsync(LLMRequest request, string model, CancellationToken cancellationToken)\n {\n // OpenAI-compatible API (LM Studio, vLLM, Text Generation WebUI, etc.)\n var requestBody = new\n {\n model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens,\n stream = false\n };\n\n var url = _options.LocalLLM.BaseUrl.TrimEnd('/');\n if (!url.EndsWith(\"/v1/chat/completions\"))\n {\n url = url.EndsWith(\"/v1\") ? $\"{url}/chat/completions\" : $\"{url}/v1/chat/completions\";\n }\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.LocalLLM.ApiKey))\n {\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", _options.LocalLLM.ApiKey);\n }\n\n foreach (var header in _options.LocalLLM.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Local LLM error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Local LLM error: {httpResponse.StatusCode}\");\n }\n\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n var content = root.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n\n int promptTokens = 0, completionTokens = 0;\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n if (usage.TryGetProperty(\"prompt_tokens\", out var pt))\n {\n promptTokens = pt.GetInt32();\n }\n if (usage.TryGetProperty(\"completion_tokens\", out var ct))\n {\n completionTokens = ct.GetInt32();\n }\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"local\",\n Model = model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n\n #region Custom Endpoint\n\n private async Task<LLMResponse> CallCustomEndpointAsync(LLMRequest request, CancellationToken cancellationToken)\n {\n var requestBody = new\n {\n model = request.Model,\n messages = request.Messages?.Select(m => new { role = m.Role, content = m.Content }).ToArray(),\n temperature = request.Temperature,\n max_tokens = request.MaxTokens\n };\n\n var httpRequest = new HttpRequestMessage(HttpMethod.Post, _options.CustomEndpoint.Url)\n {\n Content = new StringContent(\n JsonSerializer.Serialize(requestBody),\n Encoding.UTF8,\n \"application/json\")\n };\n\n if (!string.IsNullOrEmpty(_options.CustomEndpoint.ApiKey))\n {\n var customApiKey = await ResolveApiKeyAsync(\"custom\", _options.CustomEndpoint.ApiKey);\n httpRequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", customApiKey);\n }\n\n foreach (var header in _options.CustomEndpoint.Headers)\n {\n httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);\n }\n\n var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);\n var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);\n\n if (!httpResponse.IsSuccessStatusCode)\n {\n _logger.LogError(\"Custom endpoint error: {StatusCode} - {Response}\", httpResponse.StatusCode, responseJson);\n throw new HttpRequestException($\"Custom endpoint error: {httpResponse.StatusCode}\");\n }\n\n // Try to parse OpenAI-compatible response format\n using var doc = JsonDocument.Parse(responseJson);\n var root = doc.RootElement;\n\n string content;\n int promptTokens = 0, completionTokens = 0;\n\n // Handle OpenAI format\n if (root.TryGetProperty(\"choices\", out var choices))\n {\n content = choices[0].GetProperty(\"message\").GetProperty(\"content\").GetString() ?? \"\";\n if (root.TryGetProperty(\"usage\", out var usage))\n {\n usage.TryGetProperty(\"prompt_tokens\", out var pt);\n usage.TryGetProperty(\"completion_tokens\", out var ct);\n promptTokens = pt.ValueKind == JsonValueKind.Number ? pt.GetInt32() : 0;\n completionTokens = ct.ValueKind == JsonValueKind.Number ? ct.GetInt32() : 0;\n }\n }\n // Handle direct content response\n else if (root.TryGetProperty(\"content\", out var contentProp))\n {\n content = contentProp.GetString() ?? \"\";\n }\n else\n {\n content = responseJson;\n }\n\n return new LLMResponse\n {\n Success = true,\n Provider = \"custom\",\n Model = request.Model,\n Content = content,\n PromptTokens = promptTokens,\n CompletionTokens = completionTokens,\n TotalTokens = promptTokens + completionTokens\n };\n }\n\n #endregion\n}\n"}}}} |