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
@@ -39,6 +39,9 @@ public static class ApiKeysEndpoint
|
||||
app.MapGet("/api/admin/api-keys/event-scanner/status", HandleEventScannerStatus);
|
||||
app.MapGet("/api/admin/api-keys/event-scanner/log", HandleEventScannerLog);
|
||||
|
||||
// Scanner Logs (persistiert)
|
||||
app.MapGet("/api/admin/scanner/logs", HandleGetScannerLogs);
|
||||
|
||||
// User-Endpoints (für alle authentifizierten User)
|
||||
app.MapGet("/api/api-keys/available", HandleGetAvailable);
|
||||
app.MapPut("/api/api-keys/select/{keyId}", HandleSelectKey);
|
||||
@@ -179,32 +182,51 @@ public static class ApiKeysEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.Provider != "anthropic")
|
||||
switch (existing.Provider)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Check nur für Anthropic-Keys verfügbar" });
|
||||
return;
|
||||
case "anthropic":
|
||||
{
|
||||
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
|
||||
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
status = dbStatus,
|
||||
message = result.Message,
|
||||
plan = result.Plan,
|
||||
result.IsSubscription,
|
||||
result.Usage7d,
|
||||
result.Usage5h,
|
||||
result.Overage,
|
||||
status7d = result.Status7d,
|
||||
status5h = result.Status5h,
|
||||
statusOverage = result.StatusOverage
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "openai":
|
||||
{
|
||||
var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
|
||||
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null);
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message });
|
||||
break;
|
||||
}
|
||||
case "google":
|
||||
{
|
||||
var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
|
||||
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Message, null, null);
|
||||
await ctx.Response.WriteAsJsonAsync(new { status = dbStatus, message = result.Message, plan = result.Message });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = $"Check für Provider '{existing.Provider}' nicht unterstützt" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, existing.ApiKey);
|
||||
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||
|
||||
// Metadaten in DB speichern
|
||||
await db.UpdateApiKeyMeta(keyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
status = dbStatus,
|
||||
message = result.Message,
|
||||
plan = result.Plan,
|
||||
result.IsSubscription,
|
||||
result.Usage7d,
|
||||
result.Usage5h,
|
||||
result.Overage,
|
||||
status7d = result.Status7d,
|
||||
status5h = result.Status5h,
|
||||
statusOverage = result.StatusOverage
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleCheckAll(HttpContext ctx, IHttpClientFactory httpFactory, DatabaseService db)
|
||||
@@ -212,15 +234,37 @@ public static class ApiKeysEndpoint
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var allKeys = await db.GetApiKeys();
|
||||
var anthropicKeys = allKeys.Where(k => k.Provider == "anthropic").ToList();
|
||||
|
||||
var results = new List<object>();
|
||||
foreach (var key in anthropicKeys)
|
||||
|
||||
foreach (var key in allKeys)
|
||||
{
|
||||
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
|
||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, result.Plan, result.Usage5h, result.Usage7d });
|
||||
switch (key.Provider)
|
||||
{
|
||||
case "anthropic":
|
||||
{
|
||||
var result = await AnthropicKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||
var dbStatus = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Plan, result.Usage7d, result.Usage5h);
|
||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, result.Plan, result.Usage5h, result.Usage7d });
|
||||
break;
|
||||
}
|
||||
case "openai":
|
||||
{
|
||||
var result = await OpenAiKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
|
||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
|
||||
break;
|
||||
}
|
||||
case "google":
|
||||
{
|
||||
var result = await GoogleKeyChecker.CheckKeyAsync(httpFactory, key.ApiKey);
|
||||
var dbStatus = result.Status == "ok" ? "valid" : result.Status;
|
||||
await db.UpdateApiKeyMeta(key.KeyId, dbStatus, result.Message, null, null);
|
||||
results.Add(new { key.KeyId, key.Name, status = dbStatus, Plan = result.Message, Usage5h = (double?)null, Usage7d = (double?)null });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { checked_ = results.Count, results });
|
||||
@@ -377,10 +421,17 @@ public static class ApiKeysEndpoint
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
// Provider anhand des Key-Typs bestimmen
|
||||
var keyType = KeyPatterns.DetectType(sk.ApiKey);
|
||||
string provider;
|
||||
if (keyType.StartsWith("openai")) provider = "openai";
|
||||
else if (keyType == "google_api") provider = "google";
|
||||
else provider = "anthropic";
|
||||
|
||||
var record = new ApiKeyRecord
|
||||
{
|
||||
KeyId = Guid.NewGuid().ToString("N"),
|
||||
Provider = "anthropic",
|
||||
Provider = provider,
|
||||
Name = title,
|
||||
ApiKey = sk.ApiKey,
|
||||
IsPublic = false,
|
||||
@@ -507,4 +558,19 @@ public static class ApiKeysEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new { entries = log.Select(e => new { e.Time, e.Type, e.Message }) });
|
||||
}
|
||||
|
||||
private static async Task HandleGetScannerLogs(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin()) { ctx.Response.StatusCode = 403; return; }
|
||||
|
||||
var limit = int.TryParse(ctx.Request.Query["limit"], out var l) ? Math.Min(l, 1000) : 200;
|
||||
var offset = int.TryParse(ctx.Request.Query["offset"], out var o) ? o : 0;
|
||||
var type = ctx.Request.Query["type"].FirstOrDefault();
|
||||
var scanner = ctx.Request.Query["scanner"].FirstOrDefault();
|
||||
|
||||
var logs = await db.GetScannerLogs(limit, offset, type, scanner);
|
||||
var total = await db.GetScannerLogCount(type, scanner);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { logs, total });
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user