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
+108
-17
@@ -40,14 +40,28 @@ namespace Gott.Services
|
||||
|
||||
if (statusCode == 429)
|
||||
{
|
||||
// Rate-Limited → Key existiert, ist aber gerade nicht nutzbar
|
||||
// 429 kann Rate-Limit ODER Quota-Exceeded sein → Body prüfen
|
||||
try
|
||||
{
|
||||
var errBody = await resp.Content.ReadAsStringAsync();
|
||||
var errDoc = JsonDocument.Parse(errBody);
|
||||
if (errDoc.RootElement.TryGetProperty("error", out var errObj)
|
||||
&& errObj.TryGetProperty("code", out var codeProp))
|
||||
{
|
||||
var code = codeProp.GetString();
|
||||
if (code == "insufficient_quota")
|
||||
return new OpenAiCheckResult { Status = "quota_exceeded", Message = "Kein Guthaben / Quota erschöpft (429)" };
|
||||
}
|
||||
}
|
||||
catch { /* Body nicht parsbar → als Rate-Limit behandeln */ }
|
||||
|
||||
return new OpenAiCheckResult { Status = "rate_limited", Message = "Rate Limited (429)" };
|
||||
}
|
||||
|
||||
if (statusCode >= 400)
|
||||
return new OpenAiCheckResult { Status = "error", Message = "API-Fehler (" + statusCode + ")" };
|
||||
|
||||
// 200 OK → Key ist gültig
|
||||
// 200 OK → Key existiert, Modelle extrahieren
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
var doc = JsonDocument.Parse(body);
|
||||
|
||||
@@ -56,21 +70,11 @@ namespace Gott.Services
|
||||
if (resp.Headers.TryGetValues("openai-organization", out var orgValues))
|
||||
org = orgValues.FirstOrDefault();
|
||||
|
||||
// Rate-Limits aus Headern
|
||||
int? rpm = null;
|
||||
int? tpm = null;
|
||||
if (resp.Headers.TryGetValues("x-ratelimit-limit-requests", out var rpmVals)
|
||||
&& int.TryParse(rpmVals.FirstOrDefault(), out var rpmVal))
|
||||
rpm = rpmVal;
|
||||
if (resp.Headers.TryGetValues("x-ratelimit-limit-tokens", out var tpmVals)
|
||||
&& int.TryParse(tpmVals.FirstOrDefault(), out var tpmVal))
|
||||
tpm = tpmVal;
|
||||
|
||||
// Modell-Liste extrahieren (nur die interessantesten)
|
||||
string[] models = Array.Empty<string>();
|
||||
if (doc.RootElement.TryGetProperty("data", out var data))
|
||||
{
|
||||
var interestingPrefixes = new[] { "gpt-4", "gpt-3.5", "o1", "o3", "o4" };
|
||||
var interestingPrefixes = new[] { "gpt-5", "gpt-4", "gpt-3.5", "o1", "o3", "o4", "chatgpt" };
|
||||
models = data.EnumerateArray()
|
||||
.Select(m => m.GetProperty("id").GetString())
|
||||
.Where(id => interestingPrefixes.Any(p => id.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
|
||||
@@ -81,7 +85,8 @@ namespace Gott.Services
|
||||
|
||||
// Plan-Erkennung anhand der Modelle
|
||||
string plan;
|
||||
if (models.Any(m => m.StartsWith("o3", StringComparison.OrdinalIgnoreCase)
|
||||
if (models.Any(m => m.StartsWith("gpt-5", StringComparison.OrdinalIgnoreCase)
|
||||
|| m.StartsWith("o3", StringComparison.OrdinalIgnoreCase)
|
||||
|| m.StartsWith("o4", StringComparison.OrdinalIgnoreCase)))
|
||||
plan = "Tier 5+";
|
||||
else if (models.Any(m => m.StartsWith("gpt-4o", StringComparison.OrdinalIgnoreCase)))
|
||||
@@ -93,8 +98,31 @@ namespace Gott.Services
|
||||
else
|
||||
plan = "Free";
|
||||
|
||||
// Schritt 2: Echten Chat-Call mit max_tokens=1 um Quota zu prüfen
|
||||
// /v1/models gibt 200 auch bei quota_exceeded Keys!
|
||||
var usable = await CheckQuotaAsync(client, apiKey);
|
||||
|
||||
var msg = plan + " · Org: " + (org ?? "?") + " · " + models.Length + " Modelle";
|
||||
if (rpm.HasValue) msg += " · " + rpm.Value + " RPM";
|
||||
|
||||
// Rate-Limits aus dem Chat-Call-Header (falls verfügbar)
|
||||
if (usable.Rpm.HasValue) msg += " · " + usable.Rpm.Value + " RPM";
|
||||
if (usable.Tpm.HasValue) msg += " · " + usable.Tpm.Value + " TPM";
|
||||
|
||||
if (!usable.HasQuota)
|
||||
{
|
||||
var reason = usable.QuotaError == "billing_not_active"
|
||||
? "Billing inaktiv"
|
||||
: "Kein Guthaben";
|
||||
return new OpenAiCheckResult
|
||||
{
|
||||
Status = "quota_exceeded",
|
||||
Message = "⚠️ " + msg + " · " + reason,
|
||||
Organization = org,
|
||||
Models = models,
|
||||
RateLimitRpm = usable.Rpm,
|
||||
RateLimitTpm = usable.Tpm
|
||||
};
|
||||
}
|
||||
|
||||
return new OpenAiCheckResult
|
||||
{
|
||||
@@ -102,8 +130,8 @@ namespace Gott.Services
|
||||
Message = msg,
|
||||
Organization = org,
|
||||
Models = models,
|
||||
RateLimitRpm = rpm,
|
||||
RateLimitTpm = tpm
|
||||
RateLimitRpm = usable.Rpm,
|
||||
RateLimitTpm = usable.Tpm
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -111,5 +139,68 @@ namespace Gott.Services
|
||||
return new OpenAiCheckResult { Status = "error", Message = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
private class QuotaResult
|
||||
{
|
||||
public bool HasQuota { get; set; }
|
||||
public string QuotaError { get; set; }
|
||||
public int? Rpm { get; set; }
|
||||
public int? Tpm { get; set; }
|
||||
}
|
||||
|
||||
private static async Task<QuotaResult> CheckQuotaAsync(HttpClient client, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var chatReq = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
|
||||
chatReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
chatReq.Content = new StringContent(
|
||||
"{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}",
|
||||
System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
var chatResp = await client.SendAsync(chatReq);
|
||||
var chatStatus = (int)chatResp.StatusCode;
|
||||
|
||||
int? rpm = null;
|
||||
int? tpm = null;
|
||||
if (chatResp.Headers.TryGetValues("x-ratelimit-limit-requests", out var rpmVals)
|
||||
&& int.TryParse(rpmVals.FirstOrDefault(), out var rpmVal))
|
||||
rpm = rpmVal;
|
||||
if (chatResp.Headers.TryGetValues("x-ratelimit-limit-tokens", out var tpmVals)
|
||||
&& int.TryParse(tpmVals.FirstOrDefault(), out var tpmVal))
|
||||
tpm = tpmVal;
|
||||
|
||||
if (chatStatus == 429)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errBody = await chatResp.Content.ReadAsStringAsync();
|
||||
var errDoc = JsonDocument.Parse(errBody);
|
||||
if (errDoc.RootElement.TryGetProperty("error", out var errObj)
|
||||
&& errObj.TryGetProperty("code", out var codeProp)
|
||||
)
|
||||
{
|
||||
var code = codeProp.GetString();
|
||||
if (code == "insufficient_quota" || code == "billing_not_active")
|
||||
{
|
||||
return new QuotaResult { HasQuota = false, QuotaError = code, Rpm = rpm, Tpm = tpm };
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Echtes Rate-Limit → Key hat Quota, ist nur temporär blockiert
|
||||
return new QuotaResult { HasQuota = true, Rpm = rpm, Tpm = tpm };
|
||||
}
|
||||
|
||||
// 200 = funktioniert, alles andere = vermutlich trotzdem nutzbar
|
||||
return new QuotaResult { HasQuota = chatStatus < 400, Rpm = rpm, Tpm = tpm };
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Bei Netzwerkfehler etc. → optimistisch annehmen
|
||||
return new QuotaResult { HasQuota = true };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user