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

+98 -32
View File
@@ -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 });
}
}
+22 -2
View File
@@ -220,8 +220,28 @@ public static class ChatEndpoint
{
if (!string.IsNullOrEmpty(ctx.Provider))
{
// OpenAI-kompatibler Provider (z.B. Nvidia)
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, workCt: workCt, sseCt: sseCt);
// OpenAI-kompatibler Provider – für "openai" den User-Key aus DB verwenden
string openAiKeyOverride = null;
if (ctx.Provider == "openai")
{
// Zuerst prüfen ob der ausgewählte Key ein OpenAI-Key ist
if (userApiKey.Provider == "openai")
openAiKeyOverride = userApiKey.ApiKey;
else
{
// Fallback: ersten aktiven OpenAI-Key aus DB holen
openAiKeyOverride = await db.GetFirstActiveKeyByProvider("openai");
}
if (string.IsNullOrEmpty(openAiKeyOverride))
{
await SseHelper.TryWriteSse(res, "error", new { error = "Kein OpenAI API-Key vorhanden. Bitte einen OpenAI-Key in den API-Keys hinzufügen." }, sseCt);
await SseHelper.TryWriteSse(res, "done", new { chainId = ctx.ChainId }, sseCt);
return;
}
}
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, apiKeyOverride: openAiKeyOverride, workCt: workCt, sseCt: sseCt);
}
else
{
+9
View File
@@ -14,6 +14,7 @@ public static class ModelsEndpoint
app.MapGet("/api/models/nvidia", HandleNvidiaModels);
app.MapGet("/api/models/pollinations", HandlePollinationsModels);
app.MapGet("/api/models/gemini", HandleGeminiModels);
app.MapGet("/api/models/openai", HandleOpenAiModels);
app.MapPost("/api/models/refresh", HandleRefresh);
}
@@ -41,6 +42,14 @@ public static class ModelsEndpoint
return Results.Json(new { models });
}
private static IResult HandleOpenAiModels(OpenAiService openAiService)
{
var models = openAiService.GetOpenAiModels()
.Select(m => new { id = m.Id, displayName = m.DisplayName })
.ToList();
return Results.Json(new { models });
}
private static async Task<IResult> HandleRefresh(OpenAiService openAiService)
{
await openAiService.RefreshModelsFromApis();