0.1 OpenRouter-Schluessel aus appsettings.json entfernt. Laut Nutzer war er bereits inaktiv, eine Rotation beim Anbieter also nicht mehr noetig. Das Feld bleibt leer stehen, mit Hinweis daneben: der Schluessel gehoert in die Umgebungsvariable OpenRouter__ApiKey, die ohnehin Vorrang hat. Damit die Anwendung ohne Schluessel sauber laeuft, steigt OpenRouterApiClient jetzt frueh aus, statt zu senden. Vorher haette jeder Analyseversuch einen 401 erzeugt - der als Error im Fehler-Stream des Deployment Centers gelandet waere, worauf der Aufrufer die Fehlermeldung als JSON zu parsen versucht und einen zweiten Fehler erzeugt haette. Der Zustand wird einmal als Warning gemeldet. Die Konstante NotConfigured liegt im Interface, weil Application die Infrastructure-Schicht nicht referenziert. 1.2 Alle acht Nullable-Warnungen behoben, der Build ist jetzt warnungsfrei. Es waren durchweg Annotationsfragen, kein Verhalten aendert sich: * TradeRepository (6x) - die Parameterliste nimmt bewusst null auf, damit nullable Spalten als SQL-NULL geschrieben werden. Jetzt List<object?>. * TraderAnalyticsWorker - ThenInclude(o => o!.Market); der Lambda ist ein Ausdrucksbaum fuer EF und laeuft nie. * PolymarketProvider - raw.Question ?? ""; TruncateMarketStrings normalisierte den Wert einen Schritt spaeter ohnehin auf "". 1.3 Der Kategorie-Backfill holte den gesamten Marktbestand samt Event in den Speicher und verwarf den Grossteil sofort wieder. Die Einschraenkung auf Maerkte mit gefuellten Event-Tags steht jetzt in der Abfrage statt in der Schleife. Ergebnis unveraendert. Dabei einen eigenen Fehler in der Roadmap korrigiert: dort stand, dieser Backfill laufe "bei jedem Start". Das stimmt nicht - er gehoert zu RunRecalculateAllTradersAsync, der manuellen Wartungsaktion, die nur auf Knopfdruck laeuft. Die Startzeit war nie betroffen. Die falsche Aussage stand auch in STATUS.md und ist dort ebenfalls richtiggestellt. Build warnungsfrei, 126 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
3.9 KiB
C#
105 lines
3.9 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Predictalytics.Application.Interfaces;
|
|
|
|
namespace Predictalytics.Infrastructure.Providers.OpenRouter;
|
|
|
|
public class OpenRouterApiClient : IOpenRouterApiClient
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IConfiguration _config;
|
|
private readonly ILogger<OpenRouterApiClient> _logger;
|
|
|
|
/// <summary>
|
|
/// Ohne Schluessel wird gar nicht erst gesendet. Sonst quittiert OpenRouter jeden
|
|
/// Aufruf mit 401, das landet als Error im Fehler-Stream des Deployment Centers und
|
|
/// der Aufrufer versucht anschliessend, die Fehlermeldung als JSON zu lesen.
|
|
/// </summary>
|
|
private readonly bool _configured;
|
|
|
|
public OpenRouterApiClient(HttpClient httpClient, IConfiguration config, ILogger<OpenRouterApiClient> logger)
|
|
{
|
|
_httpClient = httpClient;
|
|
_config = config;
|
|
_logger = logger;
|
|
|
|
var baseUrl = _config["OpenRouter:BaseUrl"] ?? "https://openrouter.ai/api/v1";
|
|
var apiKey = _config["OpenRouter:ApiKey"];
|
|
|
|
_httpClient.BaseAddress = new Uri(baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/");
|
|
_configured = !string.IsNullOrWhiteSpace(apiKey);
|
|
if (_configured)
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
|
}
|
|
else
|
|
{
|
|
// Warning, nicht Error: eine fehlende KI-Anbindung ist ein Konfigurationszustand,
|
|
// kein Stoerfall - die uebrige Analyse laeuft vollstaendig ohne sie.
|
|
_logger.LogWarning("OpenRouter ist nicht konfiguriert (OpenRouter:ApiKey leer) - die KI-Strategieanalyse bleibt aus. Schluessel ueber die Umgebungsvariable OpenRouter__ApiKey setzen.");
|
|
}
|
|
// OpenRouter recommends adding a referer and title for ranking
|
|
_httpClient.DefaultRequestHeaders.Add("HTTP-Referer", "http://localhost");
|
|
_httpClient.DefaultRequestHeaders.Add("X-Title", "Predictalytics");
|
|
}
|
|
|
|
public async Task<string> GenerateChatCompletionAsync(string prompt, bool useManualModel = false, CancellationToken ct = default)
|
|
{
|
|
if (!_configured) return IOpenRouterApiClient.NotConfigured;
|
|
|
|
var model = useManualModel
|
|
? _config["OpenRouter:ManualAnalysisModel"] ?? "anthropic/claude-3-opus"
|
|
: _config["OpenRouter:DefaultModel"] ?? "google/gemini-flash-1.5";
|
|
|
|
var requestBody = new
|
|
{
|
|
model = model,
|
|
messages = new[]
|
|
{
|
|
new { role = "system", content = "You are an expert crypto and prediction market analyst. You analyze a trader's history and deduce their strategy, strengths, and weaknesses." },
|
|
new { role = "user", content = prompt }
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
var response = await _httpClient.PostAsJsonAsync("chat/completions", requestBody, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var result = await response.Content.ReadFromJsonAsync<OpenRouterResponse>(cancellationToken: ct);
|
|
return result?.Choices?[0]?.Message?.Content ?? "No response generated.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to generate chat completion from OpenRouter using model {Model}", model);
|
|
return $"Error: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private class OpenRouterResponse
|
|
{
|
|
[JsonPropertyName("choices")]
|
|
public Choice[]? Choices { get; set; }
|
|
}
|
|
|
|
private class Choice
|
|
{
|
|
[JsonPropertyName("message")]
|
|
public Message? Message { get; set; }
|
|
}
|
|
|
|
private class Message
|
|
{
|
|
[JsonPropertyName("content")]
|
|
public string? Content { get; set; }
|
|
}
|
|
}
|