feat: OpenAI Provider, Key-Checker Quota-Erkennung, persistierte Scanner-Logs

- OpenAI als vollwertiger Chat-Provider (Modell-Auswahl, Key-Routing, StreamChat)
- Hardcoded OpenAI-Modelle im Cache (gpt-5.x, gpt-4.1, o3, o4-mini etc.)
- /api/models/openai Endpoint, OpenAI-Optgroup im Agent-Dropdown
- Key-Routing: OpenAI-Key aus api_keys-Tabelle, Fallback via GetFirstActiveKeyByProvider
- OpenAI-Checker: 2-Schritt-Check (Models + chat/completions mit max_tokens=1)
- Google-Checker: 2-Schritt-Check (Models + generateContent mit maxOutputTokens=1)
- 429 insufficient_quota und billing_not_active korrekt als quota_exceeded erkannt
- Provider-Erkennung beim Adopt via KeyPatterns.DetectType() statt hardcoded anthropic
- HandleCheck/HandleCheckAll unterstützt jetzt Anthropic, OpenAI und Google
- Neue scanner_logs-Tabelle mit details-Spalte (JSON) für strukturierte Auswertung
- GitHubEventScannerService: AddLog persistiert in DB, detaillierte Key-Fund-Logs
- KeyScannerService: AddLog-Methode mit DB-Persistierung, detaillierte Logs
- Frontend: Ungeprüfte-prüfen-Button (sequenziell, stoppt bei valid)
- Frontend: Provider-Indicator im Key-Switch-Modal (Anthropic/OpenAI/Google)
- Frontend: Check-Button für alle Provider (nicht nur Anthropic)
This commit is contained in:
Meleki committed 2026-03-14 11:19:33 +01:00
1 parent c8859a89d0
commit 542a031172
15 files changed
+614 -90

No files matched your search

+93 -1
View File
@@ -13,8 +13,85 @@ namespace Gott.Services
public string[] Models { get; set; }
}
public class GoogleQuotaResult
{
public bool HasQuota { get; set; }
public string Error { get; set; }
}
public static class GoogleKeyChecker
{
private static async Task<GoogleQuotaResult> CheckQuotaAsync(HttpClient client, string apiKey, string[] models)
{
try
{
// Günstigstes Modell wählen (flash > pro > erstbestes)
var testModel = models.FirstOrDefault(m => m.Contains("flash", StringComparison.OrdinalIgnoreCase))
?? models.FirstOrDefault(m => m.Contains("pro", StringComparison.OrdinalIgnoreCase))
?? models.FirstOrDefault();
if (testModel == null)
return new GoogleQuotaResult { HasQuota = false, Error = "Keine Modelle verfügbar" };
var url = "https://generativelanguage.googleapis.com/v1/models/" + testModel
+ ":generateContent?key=" + apiKey;
var payload = new
{
contents = new[] { new { parts = new[] { new { text = "hi" } } } },
generationConfig = new { maxOutputTokens = 1 }
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var resp = await client.PostAsync(url, content);
var statusCode = (int)resp.StatusCode;
if (statusCode == 200)
return new GoogleQuotaResult { HasQuota = true };
var body = await resp.Content.ReadAsStringAsync();
// 429: Quota oder Rate-Limit
if (statusCode == 429)
{
if (body.Contains("RESOURCE_EXHAUSTED", StringComparison.OrdinalIgnoreCase)
&& (body.Contains("quota", StringComparison.OrdinalIgnoreCase)
|| body.Contains("limit_value", StringComparison.OrdinalIgnoreCase)))
{
// Quota auf 0 oder erschöpft
if (body.Contains("\"0\"") || body.Contains("GenerateContent"))
return new GoogleQuotaResult { HasQuota = false, Error = "Quota erschöpft (0 RPM)" };
return new GoogleQuotaResult { HasQuota = false, Error = "Quota erschöpft" };
}
// Echtes Rate-Limit → Key hat Quota, ist nur temporär blockiert
return new GoogleQuotaResult { HasQuota = true };
}
// 403: API nicht aktiviert oder Billing-Problem
if (statusCode == 403)
{
if (body.Contains("PERMISSION_DENIED", StringComparison.OrdinalIgnoreCase))
return new GoogleQuotaResult { HasQuota = false, Error = "Keine Berechtigung für generateContent" };
if (body.Contains("billing", StringComparison.OrdinalIgnoreCase))
return new GoogleQuotaResult { HasQuota = false, Error = "Billing nicht aktiviert" };
return new GoogleQuotaResult { HasQuota = false, Error = "Zugriff verweigert (403)" };
}
// Andere Fehler → konservativ als "kein Quota" behandeln
return new GoogleQuotaResult { HasQuota = false, Error = "API-Fehler (" + statusCode + ")" };
}
catch (Exception)
{
// Bei Netzwerkfehler etc. → konservativ
return new GoogleQuotaResult { HasQuota = false, Error = "Quota-Check fehlgeschlagen" };
}
}
public static async Task<GoogleCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
{
try
@@ -77,10 +154,25 @@ namespace Gott.Services
else
plan = "Free";
// Schritt 2: Echten generateContent-Call machen um Quota zu prüfen
var quota = await CheckQuotaAsync(client, apiKey, models);
var info = models.Length + " Gemini-Modelle · " + plan;
if (!quota.HasQuota)
{
info += " · " + quota.Error;
return new GoogleCheckResult
{
Status = "quota_exceeded",
Message = info,
Models = models
};
}
return new GoogleCheckResult
{
Status = "ok",
Message = models.Length + " Gemini-Modelle · " + plan,
Message = info,
Models = models
};
}