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:
1 parent
c8859a89d0
commit
542a031172
15 files changed
+614
-90
No files matched your search
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user