Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
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.
2026-08-07 12:53:47 +08:00

1 line
80 KiB
JSON

{"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/// <summary>\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/// </summary>\npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task<AiChatResult> CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> 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<AiCompletion> CompleteWithToolsAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<AiToolCall>());\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<AiCompletion> OpenAiCompatibleWithTools(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { msgs.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new { role = \"tool\", tool_call_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n msgs.Add(new\n {\n role = \"assistant\",\n content = m.Text,\n tool_calls = m.ToolCalls.Select(tc => new\n {\n id = tc.Id,\n type = \"function\",\n function = new { name = tc.Name, arguments = tc.ArgumentsJson }\n }).ToArray()\n });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : m.Role, content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n type = \"function\",\n function = new { name = t.Name, description = t.Description ?? \"\", parameters = ParseSchema(t.ParametersJsonSchema) }\n }).ToArray();\n\n var body = new { model, messages = msgs, tools = toolDefs, tool_choice = \"auto\" };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var msg = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\");\n string? text = msg.TryGetProperty(\"content\", out var cnt) && cnt.ValueKind == JsonValueKind.String ? cnt.GetString() : null;\n\n var calls = new List<AiToolCall>();\n if (msg.TryGetProperty(\"tool_calls\", out var tcs) && tcs.ValueKind == JsonValueKind.Array)\n {\n foreach (var tc in tcs.EnumerateArray())\n {\n var id = tc.TryGetProperty(\"id\", out var idEl) ? idEl.GetString() ?? \"\" : \"\";\n var fn = tc.GetProperty(\"function\");\n var name = fn.GetProperty(\"name\").GetString() ?? \"\";\n var args = fn.TryGetProperty(\"arguments\", out var a) ? (a.ValueKind == JsonValueKind.String ? a.GetString() ?? \"{}\" : a.GetRawText()) : \"{}\";\n calls.Add(new AiToolCall(id, name, args));\n }\n }\n\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiCompletion(true, text, null, inTok, outTok, calls);\n }\n\n // ---------- Tool-calling: formato nativo de Claude ----------\n private async Task<AiCompletion> ClaudeWithTools(string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> tools, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n\n var msgs = new List<object>();\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new\n {\n role = \"user\",\n content = new object[] { new { type = \"tool_result\", tool_use_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" } }\n });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n var blocks = new List<object>();\n if (!string.IsNullOrWhiteSpace(m.Text)) { blocks.Add(new { type = \"text\", text = m.Text }); }\n foreach (var tc in m.ToolCalls)\n {\n blocks.Add(new { type = \"tool_use\", id = tc.Id, name = tc.Name, input = ParseSchema(tc.ArgumentsJson) });\n }\n msgs.Add(new { role = \"assistant\", content = blocks.ToArray() });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : \"user\", content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n name = t.Name,\n description = t.Description ?? \"\",\n input_schema = ParseSchema(t.ParametersJsonSchema)\n }).ToArray();\n\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n tools = toolDefs,\n messages = msgs\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var sb = new StringBuilder();\n var calls = new List<AiToolCall>();\n foreach (var block in doc.RootElement.GetProperty(\"content\").EnumerateArray())\n {\n var type = block.GetProperty(\"type\").GetString();\n if (type == \"text\") { sb.Append(block.GetProperty(\"text\").GetString()); }\n else if (type == \"tool_use\")\n {\n var id = block.GetProperty(\"id\").GetString() ?? \"\";\n var name = block.GetProperty(\"name\").GetString() ?? \"\";\n var input = block.TryGetProperty(\"input\", out var inp) ? inp.GetRawText() : \"{}\";\n calls.Add(new AiToolCall(id, name, input));\n }\n }\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n var txt = sb.ToString();\n return new AiCompletion(true, string.IsNullOrEmpty(txt) ? null : txt, null, inTok, outTok, calls);\n }\n\n private static JsonElement ParseSchema(string? json)\n {\n try\n {\n using var d = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? \"{}\" : json);\n return d.RootElement.Clone();\n }\n catch\n {\n using var d = JsonDocument.Parse(\"{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{}}\");\n return d.RootElement.Clone();\n }\n }\n\n private static AiCompletion FailTools(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiCompletion(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\", 0, 0, Array.Empty<AiToolCall>());\n }\n\n private async Task<AiChatResult> Gemini(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/models/{model}:generateContent?key={apiKey}\";\n var body = new\n {\n systemInstruction = string.IsNullOrWhiteSpace(systemPrompt) ? null : new { parts = new[] { new { text = systemPrompt } } },\n contents = turns.Select(t => new { role = t.Role == \"model\" ? \"model\" : \"user\", parts = new[] { new { text = t.Text } } }).ToArray()\n };\n using var resp = await _http.PostAsync(url, JsonBody(body), ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"candidates\")[0].GetProperty(\"content\").GetProperty(\"parts\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usageMetadata\", out var um))\n {\n inTok = um.TryGetProperty(\"promptTokenCount\", out var p) ? p.GetInt32() : 0;\n outTok = um.TryGetProperty(\"candidatesTokenCount\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> OpenAiCompatible(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var fallback = provider == AiProvider.DeepSeek ? \"https://api.deepseek.com\" : \"https://api.openai.com/v1\";\n var url = $\"{Base(baseUrl, fallback)}/chat/completions\";\n\n var messages = new List<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { messages.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var t in turns) { messages.Add(new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }); }\n\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(new { model, messages }) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> Claude(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n messages = turns.Select(t => new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }).ToArray()\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"content\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private static StringContent JsonBody(object body) =>\n new(JsonSerializer.Serialize(body, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8, \"application/json\");\n\n private static AiChatResult Fail(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiChatResult(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\");\n }\n}\n"}, "fc5a2891fd3978d2cdd779665789349f104e1658": {"t": 1785760511.1281013, "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/// <summary>\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/// </summary>\npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task<AiChatResult> CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> 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<AiCompletion> CompleteWithToolsAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<AiToolCall>());\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<AiCompletion> OpenAiCompatibleWithTools(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { msgs.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new { role = \"tool\", tool_call_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n msgs.Add(new\n {\n role = \"assistant\",\n content = m.Text,\n tool_calls = m.ToolCalls.Select(tc => new\n {\n id = tc.Id,\n type = \"function\",\n function = new { name = tc.Name, arguments = tc.ArgumentsJson }\n }).ToArray()\n });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : m.Role, content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n type = \"function\",\n function = new { name = t.Name, description = t.Description ?? \"\", parameters = ParseSchema(t.ParametersJsonSchema) }\n }).ToArray();\n\n var body = new { model, messages = msgs, tools = toolDefs, tool_choice = \"auto\" };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var msg = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\");\n string? text = msg.TryGetProperty(\"content\", out var cnt) && cnt.ValueKind == JsonValueKind.String ? cnt.GetString() : null;\n\n var calls = new List<AiToolCall>();\n if (msg.TryGetProperty(\"tool_calls\", out var tcs) && tcs.ValueKind == JsonValueKind.Array)\n {\n foreach (var tc in tcs.EnumerateArray())\n {\n var id = tc.TryGetProperty(\"id\", out var idEl) ? idEl.GetString() ?? \"\" : \"\";\n var fn = tc.GetProperty(\"function\");\n var name = fn.GetProperty(\"name\").GetString() ?? \"\";\n var args = fn.TryGetProperty(\"arguments\", out var a) ? (a.ValueKind == JsonValueKind.String ? a.GetString() ?? \"{}\" : a.GetRawText()) : \"{}\";\n calls.Add(new AiToolCall(id, name, args));\n }\n }\n\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiCompletion(true, text, null, inTok, outTok, calls);\n }\n\n // ---------- Tool-calling: formato nativo de Claude ----------\n private async Task<AiCompletion> ClaudeWithTools(string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> tools, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n\n var msgs = new List<object>();\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new\n {\n role = \"user\",\n content = new object[] { new { type = \"tool_result\", tool_use_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" } }\n });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n var blocks = new List<object>();\n if (!string.IsNullOrWhiteSpace(m.Text)) { blocks.Add(new { type = \"text\", text = m.Text }); }\n foreach (var tc in m.ToolCalls)\n {\n blocks.Add(new { type = \"tool_use\", id = tc.Id, name = tc.Name, input = ParseSchema(tc.ArgumentsJson) });\n }\n msgs.Add(new { role = \"assistant\", content = blocks.ToArray() });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : \"user\", content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n name = t.Name,\n description = t.Description ?? \"\",\n input_schema = ParseSchema(t.ParametersJsonSchema)\n }).ToArray();\n\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n tools = toolDefs,\n messages = msgs\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var sb = new StringBuilder();\n var calls = new List<AiToolCall>();\n foreach (var block in doc.RootElement.GetProperty(\"content\").EnumerateArray())\n {\n var type = block.GetProperty(\"type\").GetString();\n if (type == \"text\") { sb.Append(block.GetProperty(\"text\").GetString()); }\n else if (type == \"tool_use\")\n {\n var id = block.GetProperty(\"id\").GetString() ?? \"\";\n var name = block.GetProperty(\"name\").GetString() ?? \"\";\n var input = block.TryGetProperty(\"input\", out var inp) ? inp.GetRawText() : \"{}\";\n calls.Add(new AiToolCall(id, name, input));\n }\n }\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n var txt = sb.ToString();\n return new AiCompletion(true, string.IsNullOrEmpty(txt) ? null : txt, null, inTok, outTok, calls);\n }\n\n private static JsonElement ParseSchema(string? json)\n {\n try\n {\n using var d = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? \"{}\" : json);\n return d.RootElement.Clone();\n }\n catch\n {\n using var d = JsonDocument.Parse(\"{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{}}\");\n return d.RootElement.Clone();\n }\n }\n\n private static AiCompletion FailTools(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiCompletion(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\", 0, 0, Array.Empty<AiToolCall>());\n }\n\n private async Task<AiChatResult> Gemini(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/models/{model}:generateContent?key={apiKey}\";\n var body = new\n {\n systemInstruction = string.IsNullOrWhiteSpace(systemPrompt) ? null : new { parts = new[] { new { text = systemPrompt } } },\n contents = turns.Select(t => new { role = t.Role == \"model\" ? \"model\" : \"user\", parts = new[] { new { text = t.Text } } }).ToArray()\n };\n using var resp = await _http.PostAsync(url, JsonBody(body), ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"candidates\")[0].GetProperty(\"content\").GetProperty(\"parts\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usageMetadata\", out var um))\n {\n inTok = um.TryGetProperty(\"promptTokenCount\", out var p) ? p.GetInt32() : 0;\n outTok = um.TryGetProperty(\"candidatesTokenCount\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> OpenAiCompatible(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var fallback = provider == AiProvider.DeepSeek ? \"https://api.deepseek.com\" : \"https://api.openai.com/v1\";\n var url = $\"{Base(baseUrl, fallback)}/chat/completions\";\n\n var messages = new List<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { messages.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var t in turns) { messages.Add(new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }); }\n\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(new { model, messages }) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> Claude(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n messages = turns.Select(t => new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }).ToArray()\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"content\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private static StringContent JsonBody(object body) =>\n new(JsonSerializer.Serialize(body, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8, \"application/json\");\n\n private static AiChatResult Fail(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiChatResult(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\");\n }\n}\n"}, "e83e3bca67b24697be1ddb90ae2538ad1cd63b56": {"t": 1785760512.3289473, "n": 6609, "h": "9b9de98ac4fed107", "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/// <summary>\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/// </summary>\npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task<AiChatResult> CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> 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 private static string Base(string? baseUrl, string fallback) =>\n (string.IsNullOrWhiteSpace(baseUrl) ? fallback : baseUrl).TrimEnd('/');\n\n private async Task<AiChatResult> Gemini(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/models/{model}:generateContent?key={apiKey}\";\n var body = new\n {\n systemInstruction = string.IsNullOrWhiteSpace(systemPrompt) ? null : new { parts = new[] { new { text = systemPrompt } } },\n contents = turns.Select(t => new { role = t.Role == \"model\" ? \"model\" : \"user\", parts = new[] { new { text = t.Text } } }).ToArray()\n };\n using var resp = await _http.PostAsync(url, JsonBody(body), ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"candidates\")[0].GetProperty(\"content\").GetProperty(\"parts\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usageMetadata\", out var um))\n {\n inTok = um.TryGetProperty(\"promptTokenCount\", out var p) ? p.GetInt32() : 0;\n outTok = um.TryGetProperty(\"candidatesTokenCount\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> OpenAiCompatible(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var fallback = provider == AiProvider.DeepSeek ? \"https://api.deepseek.com\" : \"https://api.openai.com/v1\";\n var url = $\"{Base(baseUrl, fallback)}/chat/completions\";\n\n var messages = new List<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { messages.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var t in turns) { messages.Add(new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }); }\n\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(new { model, messages }) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> Claude(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n messages = turns.Select(t => new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }).ToArray()\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"content\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private static StringContent JsonBody(object body) =>\n new(JsonSerializer.Serialize(body, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8, \"application/json\");\n\n private static AiChatResult Fail(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiChatResult(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\");\n }\n}\n"}, "11cc8a6664aceef61f76372a0042ba749aa3530d": {"t": 1786067627.1318882, "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/// <summary>\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/// </summary>\npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task<AiChatResult> CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> 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<AiCompletion> CompleteWithToolsAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<AiToolCall>());\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<AiCompletion> OpenAiCompatibleWithTools(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { msgs.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new { role = \"tool\", tool_call_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n msgs.Add(new\n {\n role = \"assistant\",\n content = m.Text,\n tool_calls = m.ToolCalls.Select(tc => new\n {\n id = tc.Id,\n type = \"function\",\n function = new { name = tc.Name, arguments = tc.ArgumentsJson }\n }).ToArray()\n });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : m.Role, content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n type = \"function\",\n function = new { name = t.Name, description = t.Description ?? \"\", parameters = ParseSchema(t.ParametersJsonSchema) }\n }).ToArray();\n\n var body = new { model, messages = msgs, tools = toolDefs, tool_choice = \"auto\" };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var msg = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\");\n string? text = msg.TryGetProperty(\"content\", out var cnt) && cnt.ValueKind == JsonValueKind.String ? cnt.GetString() : null;\n\n var calls = new List<AiToolCall>();\n if (msg.TryGetProperty(\"tool_calls\", out var tcs) && tcs.ValueKind == JsonValueKind.Array)\n {\n foreach (var tc in tcs.EnumerateArray())\n {\n var id = tc.TryGetProperty(\"id\", out var idEl) ? idEl.GetString() ?? \"\" : \"\";\n var fn = tc.GetProperty(\"function\");\n var name = fn.GetProperty(\"name\").GetString() ?? \"\";\n var args = fn.TryGetProperty(\"arguments\", out var a) ? (a.ValueKind == JsonValueKind.String ? a.GetString() ?? \"{}\" : a.GetRawText()) : \"{}\";\n calls.Add(new AiToolCall(id, name, args));\n }\n }\n\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiCompletion(true, text, null, inTok, outTok, calls);\n }\n\n // ---------- Tool-calling: formato nativo de Claude ----------\n private async Task<AiCompletion> ClaudeWithTools(string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> tools, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n\n var msgs = new List<object>();\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new\n {\n role = \"user\",\n content = new object[] { new { type = \"tool_result\", tool_use_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" } }\n });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n var blocks = new List<object>();\n if (!string.IsNullOrWhiteSpace(m.Text)) { blocks.Add(new { type = \"text\", text = m.Text }); }\n foreach (var tc in m.ToolCalls)\n {\n blocks.Add(new { type = \"tool_use\", id = tc.Id, name = tc.Name, input = ParseSchema(tc.ArgumentsJson) });\n }\n msgs.Add(new { role = \"assistant\", content = blocks.ToArray() });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : \"user\", content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n name = t.Name,\n description = t.Description ?? \"\",\n input_schema = ParseSchema(t.ParametersJsonSchema)\n }).ToArray();\n\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n tools = toolDefs,\n messages = msgs\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var sb = new StringBuilder();\n var calls = new List<AiToolCall>();\n foreach (var block in doc.RootElement.GetProperty(\"content\").EnumerateArray())\n {\n var type = block.GetProperty(\"type\").GetString();\n if (type == \"text\") { sb.Append(block.GetProperty(\"text\").GetString()); }\n else if (type == \"tool_use\")\n {\n var id = block.GetProperty(\"id\").GetString() ?? \"\";\n var name = block.GetProperty(\"name\").GetString() ?? \"\";\n var input = block.TryGetProperty(\"input\", out var inp) ? inp.GetRawText() : \"{}\";\n calls.Add(new AiToolCall(id, name, input));\n }\n }\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n var txt = sb.ToString();\n return new AiCompletion(true, string.IsNullOrEmpty(txt) ? null : txt, null, inTok, outTok, calls);\n }\n\n private static JsonElement ParseSchema(string? json)\n {\n try\n {\n using var d = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? \"{}\" : json);\n return d.RootElement.Clone();\n }\n catch\n {\n using var d = JsonDocument.Parse(\"{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{}}\");\n return d.RootElement.Clone();\n }\n }\n\n private static AiCompletion FailTools(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiCompletion(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\", 0, 0, Array.Empty<AiToolCall>());\n }\n\n private async Task<AiChatResult> Gemini(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/models/{model}:generateContent?key={apiKey}\";\n var body = new\n {\n systemInstruction = string.IsNullOrWhiteSpace(systemPrompt) ? null : new { parts = new[] { new { text = systemPrompt } } },\n contents = turns.Select(t => new { role = t.Role == \"model\" ? \"model\" : \"user\", parts = new[] { new { text = t.Text } } }).ToArray()\n };\n using var resp = await _http.PostAsync(url, JsonBody(body), ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"candidates\")[0].GetProperty(\"content\").GetProperty(\"parts\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usageMetadata\", out var um))\n {\n inTok = um.TryGetProperty(\"promptTokenCount\", out var p) ? p.GetInt32() : 0;\n outTok = um.TryGetProperty(\"candidatesTokenCount\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> OpenAiCompatible(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var fallback = provider == AiProvider.DeepSeek ? \"https://api.deepseek.com\" : \"https://api.openai.com/v1\";\n var url = $\"{Base(baseUrl, fallback)}/chat/completions\";\n\n var messages = new List<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { messages.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var t in turns) { messages.Add(new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }); }\n\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(new { model, messages }) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> Claude(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n messages = turns.Select(t => new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }).ToArray()\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"content\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private static StringContent JsonBody(object body) =>\n new(JsonSerializer.Serialize(body, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8, \"application/json\");\n\n private static AiChatResult Fail(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiChatResult(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\");\n }\n}\n"}, "9d6d011e0a230fb721da3c5fac5a92022314f2bd": {"t": 1786068387.9743948, "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/// <summary>\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/// </summary>\npublic sealed class AiProviderClient : IAiProviderClient\n{\n private readonly HttpClient _http;\n\n public AiProviderClient(HttpClient http) => _http = http;\n\n public async Task<AiChatResult> CompleteAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> 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<AiCompletion> CompleteWithToolsAsync(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<AiToolCall>());\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<AiCompletion> OpenAiCompatibleWithTools(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> 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<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { msgs.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new { role = \"tool\", tool_call_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n msgs.Add(new\n {\n role = \"assistant\",\n content = m.Text,\n tool_calls = m.ToolCalls.Select(tc => new\n {\n id = tc.Id,\n type = \"function\",\n function = new { name = tc.Name, arguments = tc.ArgumentsJson }\n }).ToArray()\n });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : m.Role, content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n type = \"function\",\n function = new { name = t.Name, description = t.Description ?? \"\", parameters = ParseSchema(t.ParametersJsonSchema) }\n }).ToArray();\n\n var body = new { model, messages = msgs, tools = toolDefs, tool_choice = \"auto\" };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var msg = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\");\n string? text = msg.TryGetProperty(\"content\", out var cnt) && cnt.ValueKind == JsonValueKind.String ? cnt.GetString() : null;\n\n var calls = new List<AiToolCall>();\n if (msg.TryGetProperty(\"tool_calls\", out var tcs) && tcs.ValueKind == JsonValueKind.Array)\n {\n foreach (var tc in tcs.EnumerateArray())\n {\n var id = tc.TryGetProperty(\"id\", out var idEl) ? idEl.GetString() ?? \"\" : \"\";\n var fn = tc.GetProperty(\"function\");\n var name = fn.GetProperty(\"name\").GetString() ?? \"\";\n var args = fn.TryGetProperty(\"arguments\", out var a) ? (a.ValueKind == JsonValueKind.String ? a.GetString() ?? \"{}\" : a.GetRawText()) : \"{}\";\n calls.Add(new AiToolCall(id, name, args));\n }\n }\n\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiCompletion(true, text, null, inTok, outTok, calls);\n }\n\n // ---------- Tool-calling: formato nativo de Claude ----------\n private async Task<AiCompletion> ClaudeWithTools(string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiToolMessage> messages, IReadOnlyList<AiToolSpec> tools, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n\n var msgs = new List<object>();\n foreach (var m in messages)\n {\n if (m.Role == \"tool\")\n {\n msgs.Add(new\n {\n role = \"user\",\n content = new object[] { new { type = \"tool_result\", tool_use_id = m.ToolCallId ?? \"\", content = m.Text ?? \"\" } }\n });\n }\n else if (m.Role is \"assistant\" or \"model\" && m.ToolCalls is { Count: > 0 })\n {\n var blocks = new List<object>();\n if (!string.IsNullOrWhiteSpace(m.Text)) { blocks.Add(new { type = \"text\", text = m.Text }); }\n foreach (var tc in m.ToolCalls)\n {\n blocks.Add(new { type = \"tool_use\", id = tc.Id, name = tc.Name, input = ParseSchema(tc.ArgumentsJson) });\n }\n msgs.Add(new { role = \"assistant\", content = blocks.ToArray() });\n }\n else\n {\n msgs.Add(new { role = m.Role == \"model\" ? \"assistant\" : \"user\", content = m.Text ?? \"\" });\n }\n }\n\n var toolDefs = tools.Select(t => new\n {\n name = t.Name,\n description = t.Description ?? \"\",\n input_schema = ParseSchema(t.ParametersJsonSchema)\n }).ToArray();\n\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n tools = toolDefs,\n messages = msgs\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return FailTools((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var sb = new StringBuilder();\n var calls = new List<AiToolCall>();\n foreach (var block in doc.RootElement.GetProperty(\"content\").EnumerateArray())\n {\n var type = block.GetProperty(\"type\").GetString();\n if (type == \"text\") { sb.Append(block.GetProperty(\"text\").GetString()); }\n else if (type == \"tool_use\")\n {\n var id = block.GetProperty(\"id\").GetString() ?? \"\";\n var name = block.GetProperty(\"name\").GetString() ?? \"\";\n var input = block.TryGetProperty(\"input\", out var inp) ? inp.GetRawText() : \"{}\";\n calls.Add(new AiToolCall(id, name, input));\n }\n }\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n var txt = sb.ToString();\n return new AiCompletion(true, string.IsNullOrEmpty(txt) ? null : txt, null, inTok, outTok, calls);\n }\n\n private static JsonElement ParseSchema(string? json)\n {\n try\n {\n using var d = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? \"{}\" : json);\n return d.RootElement.Clone();\n }\n catch\n {\n using var d = JsonDocument.Parse(\"{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{}}\");\n return d.RootElement.Clone();\n }\n }\n\n private static AiCompletion FailTools(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiCompletion(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\", 0, 0, Array.Empty<AiToolCall>());\n }\n\n private async Task<AiChatResult> Gemini(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://generativelanguage.googleapis.com\")}/v1beta/models/{model}:generateContent?key={apiKey}\";\n var body = new\n {\n systemInstruction = string.IsNullOrWhiteSpace(systemPrompt) ? null : new { parts = new[] { new { text = systemPrompt } } },\n contents = turns.Select(t => new { role = t.Role == \"model\" ? \"model\" : \"user\", parts = new[] { new { text = t.Text } } }).ToArray()\n };\n using var resp = await _http.PostAsync(url, JsonBody(body), ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"candidates\")[0].GetProperty(\"content\").GetProperty(\"parts\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usageMetadata\", out var um))\n {\n inTok = um.TryGetProperty(\"promptTokenCount\", out var p) ? p.GetInt32() : 0;\n outTok = um.TryGetProperty(\"candidatesTokenCount\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> OpenAiCompatible(AiProvider provider, string apiKey, string? baseUrl, string model,\n string systemPrompt, IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var fallback = provider == AiProvider.DeepSeek ? \"https://api.deepseek.com\" : \"https://api.openai.com/v1\";\n var url = $\"{Base(baseUrl, fallback)}/chat/completions\";\n\n var messages = new List<object>();\n if (!string.IsNullOrWhiteSpace(systemPrompt)) { messages.Add(new { role = \"system\", content = systemPrompt }); }\n foreach (var t in turns) { messages.Add(new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }); }\n\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(new { model, messages }) };\n req.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", apiKey);\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"choices\")[0].GetProperty(\"message\").GetProperty(\"content\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"prompt_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"completion_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private async Task<AiChatResult> Claude(string apiKey, string? baseUrl, string model, string systemPrompt,\n IReadOnlyList<AiChatTurn> turns, CancellationToken ct)\n {\n var url = $\"{Base(baseUrl, \"https://api.anthropic.com\")}/v1/messages\";\n var body = new\n {\n model,\n max_tokens = 1024,\n system = string.IsNullOrWhiteSpace(systemPrompt) ? null : systemPrompt,\n messages = turns.Select(t => new { role = t.Role == \"model\" ? \"assistant\" : \"user\", content = t.Text }).ToArray()\n };\n using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonBody(body) };\n req.Headers.Add(\"x-api-key\", apiKey);\n req.Headers.Add(\"anthropic-version\", \"2023-06-01\");\n using var resp = await _http.SendAsync(req, ct);\n var raw = await resp.Content.ReadAsStringAsync(ct);\n if (!resp.IsSuccessStatusCode) { return Fail((int)resp.StatusCode, raw); }\n\n using var doc = JsonDocument.Parse(raw);\n var text = doc.RootElement.GetProperty(\"content\")[0].GetProperty(\"text\").GetString();\n var (inTok, outTok) = (0, 0);\n if (doc.RootElement.TryGetProperty(\"usage\", out var u))\n {\n inTok = u.TryGetProperty(\"input_tokens\", out var p) ? p.GetInt32() : 0;\n outTok = u.TryGetProperty(\"output_tokens\", out var c) ? c.GetInt32() : 0;\n }\n return new AiChatResult(true, text, null, inTok, outTok);\n }\n\n private static StringContent JsonBody(object body) =>\n new(JsonSerializer.Serialize(body, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }),\n Encoding.UTF8, \"application/json\");\n\n private static AiChatResult Fail(int status, string raw)\n {\n var snippet = raw.Length > 300 ? raw[..300] : raw;\n return new AiChatResult(false, null, $\"El proveedor respondio HTTP {status}. {snippet}\");\n }\n}\n"}}, "src/Propia.Application/Integraciones/IAiServerConfigService.cs": {"a0ef0fc5f0411e8383733837c2ceb38531bd2cef": {"t": 1785746202.8392532, "n": 2364, "h": "8da62b25d8591818", "c": "using Propia.Domain.Enums;\n\nnamespace Propia.Application.Integraciones;\n\npublic sealed record AiProviderDto(\n AiProvider Provider,\n string DisplayName,\n string? Model,\n string? BaseUrl,\n string? ApiKeyMasked,\n bool HasApiKey,\n bool IsEnabled,\n string DefaultModel,\n IReadOnlyList<string> SuggestedModels);\n\npublic sealed record SaveAiProviderRequest(\n AiProvider Provider,\n string? ApiKey,\n string? Model,\n string? BaseUrl,\n bool IsEnabled);\n\n/// <summary>Proveedor habilitado disponible para uso de la plataforma (sin datos sensibles).</summary>\npublic sealed record AiProviderOptionDto(AiProvider Provider, string DisplayName, string DefaultModel);\n\n/// <summary>\n/// Cuentas maestras de IA de la plataforma (Super Admin). Un registro por proveedor; la API key\n/// se cifra con ISecretProtector y nunca se devuelve en claro ni se loggea.\n/// </summary>\npublic interface IAiServerConfigService\n{\n Task<IReadOnlyList<AiProviderDto>> ListAsync(CancellationToken ct = default);\n Task<AiProviderDto> SaveAsync(SaveAiProviderRequest request, Guid actorId, string actorEmail, string? ip, CancellationToken ct = default);\n Task<IReadOnlyList<AiProviderOptionDto>> ListEnabledAsync(CancellationToken ct = default);\n}\n\n/// <summary>Metadata estatica de cada proveedor (nombre visible, modelo por defecto, modelos sugeridos).</summary>\npublic static class AiProviderCatalog\n{\n public sealed record Meta(string DisplayName, string DefaultModel, IReadOnlyList<string> Models, string? DefaultBaseUrl);\n\n public static Meta For(AiProvider p) => p switch\n {\n AiProvider.Claude => new(\"Anthropic Claude\", \"claude-opus-4-7\",\n new[] { \"claude-opus-4-7\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\" }, \"https://api.anthropic.com\"),\n AiProvider.Gemini => new(\"Google Gemini\", \"gemini-2.5-pro\",\n new[] { \"gemini-2.5-pro\", \"gemini-2.5-flash\", \"gemini-2.0-flash\" }, \"https://generativelanguage.googleapis.com\"),\n AiProvider.ChatGpt => new(\"OpenAI ChatGPT\", \"gpt-4o\",\n new[] { \"gpt-4o\", \"gpt-4o-mini\", \"o3\", \"o3-mini\" }, \"https://api.openai.com/v1\"),\n AiProvider.DeepSeek => new(\"DeepSeek\", \"deepseek-chat\",\n new[] { \"deepseek-chat\", \"deepseek-reasoner\" }, \"https://api.deepseek.com\"),\n _ => new(p.ToString(), \"\", Array.Empty<string>(), null)\n };\n}\n"}, "1b787227e4171e581f254c112e8ddde54df6682b": {"t": 1785760510.4884474, "n": 2364, "h": "8da62b25d8591818", "c": "using Propia.Domain.Enums;\n\nnamespace Propia.Application.Integraciones;\n\npublic sealed record AiProviderDto(\n AiProvider Provider,\n string DisplayName,\n string? Model,\n string? BaseUrl,\n string? ApiKeyMasked,\n bool HasApiKey,\n bool IsEnabled,\n string DefaultModel,\n IReadOnlyList<string> SuggestedModels);\n\npublic sealed record SaveAiProviderRequest(\n AiProvider Provider,\n string? ApiKey,\n string? Model,\n string? BaseUrl,\n bool IsEnabled);\n\n/// <summary>Proveedor habilitado disponible para uso de la plataforma (sin datos sensibles).</summary>\npublic sealed record AiProviderOptionDto(AiProvider Provider, string DisplayName, string DefaultModel);\n\n/// <summary>\n/// Cuentas maestras de IA de la plataforma (Super Admin). Un registro por proveedor; la API key\n/// se cifra con ISecretProtector y nunca se devuelve en claro ni se loggea.\n/// </summary>\npublic interface IAiServerConfigService\n{\n Task<IReadOnlyList<AiProviderDto>> ListAsync(CancellationToken ct = default);\n Task<AiProviderDto> SaveAsync(SaveAiProviderRequest request, Guid actorId, string actorEmail, string? ip, CancellationToken ct = default);\n Task<IReadOnlyList<AiProviderOptionDto>> ListEnabledAsync(CancellationToken ct = default);\n}\n\n/// <summary>Metadata estatica de cada proveedor (nombre visible, modelo por defecto, modelos sugeridos).</summary>\npublic static class AiProviderCatalog\n{\n public sealed record Meta(string DisplayName, string DefaultModel, IReadOnlyList<string> Models, string? DefaultBaseUrl);\n\n public static Meta For(AiProvider p) => p switch\n {\n AiProvider.Claude => new(\"Anthropic Claude\", \"claude-opus-4-7\",\n new[] { \"claude-opus-4-7\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\" }, \"https://api.anthropic.com\"),\n AiProvider.Gemini => new(\"Google Gemini\", \"gemini-2.5-pro\",\n new[] { \"gemini-2.5-pro\", \"gemini-2.5-flash\", \"gemini-2.0-flash\" }, \"https://generativelanguage.googleapis.com\"),\n AiProvider.ChatGpt => new(\"OpenAI ChatGPT\", \"gpt-4o\",\n new[] { \"gpt-4o\", \"gpt-4o-mini\", \"o3\", \"o3-mini\" }, \"https://api.openai.com/v1\"),\n AiProvider.DeepSeek => new(\"DeepSeek\", \"deepseek-chat\",\n new[] { \"deepseek-chat\", \"deepseek-reasoner\" }, \"https://api.deepseek.com\"),\n _ => new(p.ToString(), \"\", Array.Empty<string>(), null)\n };\n}\n"}, "11cc8a6664aceef61f76372a0042ba749aa3530d": {"t": 1786067631.904782, "n": 2364, "h": "8da62b25d8591818", "c": "using Propia.Domain.Enums;\n\nnamespace Propia.Application.Integraciones;\n\npublic sealed record AiProviderDto(\n AiProvider Provider,\n string DisplayName,\n string? Model,\n string? BaseUrl,\n string? ApiKeyMasked,\n bool HasApiKey,\n bool IsEnabled,\n string DefaultModel,\n IReadOnlyList<string> SuggestedModels);\n\npublic sealed record SaveAiProviderRequest(\n AiProvider Provider,\n string? ApiKey,\n string? Model,\n string? BaseUrl,\n bool IsEnabled);\n\n/// <summary>Proveedor habilitado disponible para uso de la plataforma (sin datos sensibles).</summary>\npublic sealed record AiProviderOptionDto(AiProvider Provider, string DisplayName, string DefaultModel);\n\n/// <summary>\n/// Cuentas maestras de IA de la plataforma (Super Admin). Un registro por proveedor; la API key\n/// se cifra con ISecretProtector y nunca se devuelve en claro ni se loggea.\n/// </summary>\npublic interface IAiServerConfigService\n{\n Task<IReadOnlyList<AiProviderDto>> ListAsync(CancellationToken ct = default);\n Task<AiProviderDto> SaveAsync(SaveAiProviderRequest request, Guid actorId, string actorEmail, string? ip, CancellationToken ct = default);\n Task<IReadOnlyList<AiProviderOptionDto>> ListEnabledAsync(CancellationToken ct = default);\n}\n\n/// <summary>Metadata estatica de cada proveedor (nombre visible, modelo por defecto, modelos sugeridos).</summary>\npublic static class AiProviderCatalog\n{\n public sealed record Meta(string DisplayName, string DefaultModel, IReadOnlyList<string> Models, string? DefaultBaseUrl);\n\n public static Meta For(AiProvider p) => p switch\n {\n AiProvider.Claude => new(\"Anthropic Claude\", \"claude-opus-4-7\",\n new[] { \"claude-opus-4-7\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\" }, \"https://api.anthropic.com\"),\n AiProvider.Gemini => new(\"Google Gemini\", \"gemini-2.5-pro\",\n new[] { \"gemini-2.5-pro\", \"gemini-2.5-flash\", \"gemini-2.0-flash\" }, \"https://generativelanguage.googleapis.com\"),\n AiProvider.ChatGpt => new(\"OpenAI ChatGPT\", \"gpt-4o\",\n new[] { \"gpt-4o\", \"gpt-4o-mini\", \"o3\", \"o3-mini\" }, \"https://api.openai.com/v1\"),\n AiProvider.DeepSeek => new(\"DeepSeek\", \"deepseek-chat\",\n new[] { \"deepseek-chat\", \"deepseek-reasoner\" }, \"https://api.deepseek.com\"),\n _ => new(p.ToString(), \"\", Array.Empty<string>(), null)\n };\n}\n"}, "9d6d011e0a230fb721da3c5fac5a92022314f2bd": {"t": 1786068389.3476906, "n": 2364, "h": "8da62b25d8591818", "c": "using Propia.Domain.Enums;\n\nnamespace Propia.Application.Integraciones;\n\npublic sealed record AiProviderDto(\n AiProvider Provider,\n string DisplayName,\n string? Model,\n string? BaseUrl,\n string? ApiKeyMasked,\n bool HasApiKey,\n bool IsEnabled,\n string DefaultModel,\n IReadOnlyList<string> SuggestedModels);\n\npublic sealed record SaveAiProviderRequest(\n AiProvider Provider,\n string? ApiKey,\n string? Model,\n string? BaseUrl,\n bool IsEnabled);\n\n/// <summary>Proveedor habilitado disponible para uso de la plataforma (sin datos sensibles).</summary>\npublic sealed record AiProviderOptionDto(AiProvider Provider, string DisplayName, string DefaultModel);\n\n/// <summary>\n/// Cuentas maestras de IA de la plataforma (Super Admin). Un registro por proveedor; la API key\n/// se cifra con ISecretProtector y nunca se devuelve en claro ni se loggea.\n/// </summary>\npublic interface IAiServerConfigService\n{\n Task<IReadOnlyList<AiProviderDto>> ListAsync(CancellationToken ct = default);\n Task<AiProviderDto> SaveAsync(SaveAiProviderRequest request, Guid actorId, string actorEmail, string? ip, CancellationToken ct = default);\n Task<IReadOnlyList<AiProviderOptionDto>> ListEnabledAsync(CancellationToken ct = default);\n}\n\n/// <summary>Metadata estatica de cada proveedor (nombre visible, modelo por defecto, modelos sugeridos).</summary>\npublic static class AiProviderCatalog\n{\n public sealed record Meta(string DisplayName, string DefaultModel, IReadOnlyList<string> Models, string? DefaultBaseUrl);\n\n public static Meta For(AiProvider p) => p switch\n {\n AiProvider.Claude => new(\"Anthropic Claude\", \"claude-opus-4-7\",\n new[] { \"claude-opus-4-7\", \"claude-sonnet-4-6\", \"claude-haiku-4-5\" }, \"https://api.anthropic.com\"),\n AiProvider.Gemini => new(\"Google Gemini\", \"gemini-2.5-pro\",\n new[] { \"gemini-2.5-pro\", \"gemini-2.5-flash\", \"gemini-2.0-flash\" }, \"https://generativelanguage.googleapis.com\"),\n AiProvider.ChatGpt => new(\"OpenAI ChatGPT\", \"gpt-4o\",\n new[] { \"gpt-4o\", \"gpt-4o-mini\", \"o3\", \"o3-mini\" }, \"https://api.openai.com/v1\"),\n AiProvider.DeepSeek => new(\"DeepSeek\", \"deepseek-chat\",\n new[] { \"deepseek-chat\", \"deepseek-reasoner\" }, \"https://api.deepseek.com\"),\n _ => new(p.ToString(), \"\", Array.Empty<string>(), null)\n };\n}\n"}}}}