{"repo": "alexandercuartas665/propia", "files": {"src/Propia.Infrastructure/InfraestructuraIa/AiProviderClient.cs": {"a0ef0fc5f0411e8383733837c2ceb38531bd2cef": {"t": 1785746193.6513298, "n": 15434, "h": "c3105b58420e4dde", "c": "using System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\nusing Propia.Application.InfraestructuraIa;\nusing Propia.Domain.Enums;\n\nnamespace Propia.Infrastructure.InfraestructuraIa;\n\n/// \n/// Cliente HTTP de inferencia para los proveedores de IA. La API key llega descifrada; no se persiste\n/// ni se loggea. Soporta Gemini (REST), OpenAI/ChatGPT y DeepSeek (chat/completions) y Claude (messages).\n/// Portado de CUBOT.travels (read-only).\n/// \npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList turns, CancellationToken ct = default)\n {\n try\n {\n return provider switch\n {\n AiProvider.Gemini => await Gemini(apiKey, baseUrl, model, systemPrompt, turns, ct),\n AiProvider.Claude => await Claude(apiKey, baseUrl, model, systemPrompt, turns, ct),\n _ => await OpenAiCompatible(provider, apiKey, baseUrl, model, systemPrompt, turns, ct)\n };\n }\n catch (Exception ex)\n {\n return new AiChatResult(false, null, $\"No se pudo contactar al proveedor: {ex.Message}\");\n }\n }\n\n public async Task CompleteWithToolsAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList messages, IReadOnlyList tools, CancellationToken ct = default)\n {\n try\n {\n return provider == AiProvider.Claude\n ? await ClaudeWithTools(apiKey, baseUrl, model, systemPrompt, messages, tools, ct)\n : await OpenAiCompatibleWithTools(provider, apiKey, baseUrl, model, systemPrompt, messages, tools, ct);\n }\n catch (Exception ex)\n {\n return new AiCompletion(false, null, $\"No se pudo contactar al proveedor: {ex.Message}\", 0, 0, Array.Empty());\n }\n }\n\n private static string Base(string? baseUrl, string fallback) =>\n (string.IsNullOrWhiteSpace(baseUrl) ? fallback : baseUrl).TrimEnd('/');\n\n // ---------- Tool-calling: formato OpenAI (OpenAI / DeepSeek / Gemini via endpoint OpenAI) ----------\n private async Task OpenAiCompatibleWithTools(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList messages, IReadOnlyList tools, CancellationToken ct)\n {\n var url = provider switch\n {\n AiProvider.Gemini => $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/openai/chat/completions\",\n AiProvider.DeepSeek => $\"{Base(baseUrl, \"https://api.deepseek.com\")}/chat/completions\",\n _ => $\"{Base(baseUrl, \"https://api.openai.com/v1\")}/chat/completions\"\n };\n\n var msgs = new List