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 { private const string CheckModel = "claude-sonnet-4-20250514"; public static async Task CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey) { try { var client = httpFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(30); var payload = JsonSerializer.Serialize(new { model = CheckModel, max_tokens = 1, messages = new[] { new { role = "user", content = "hi" } } }); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); request.Headers.Add("anthropic-version", "2023-06-01"); request.Headers.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"); request.Headers.Add("User-Agent", "claude-cli/2.1.2 (external, cli)"); request.Headers.Add("x-app", "cli"); request.Headers.Add("anthropic-dangerous-direct-browser-access", "true"); request.Content = new StringContent(payload, Encoding.UTF8, "application/json"); var resp = await client.SendAsync(request); var statusCode = (int)resp.StatusCode; var headers = resp.Headers; if (statusCode == 401) { var has7d = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization"); var has5h = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization"); if (has7d == null && has5h == null) return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" }; } else if (statusCode == 429) { return new KeyCheckResult { Status = "rate_limited", Message = "Rate Limited (429)", Plan = "Unknown" }; } else if (statusCode == 403) { return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" }; } else if (statusCode >= 400) { return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" }; } // Header auslesen 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 = statusCode == 401 ? "usage_limited" : "ok", Message = statusCode == 401 ? "Gültig, aber Usage-Limit erreicht (401)" : null, 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" }; } } /// /// Extrahiert Plan/Usage-Metadaten nur aus Response-Headern (kein eigener HTTP-Request). /// Kann null zurückgeben wenn keine Usage-Header vorhanden sind. /// public static KeyCheckResult ExtractMetaFromHeaders(HttpResponseHeaders 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"); if (d7Util == null && d5hUtil == null) return null; // Keine Usage-Header → nichts zu updaten var overageUtil = GetHeader(headers, "anthropic-ratelimit-unified-overage-utilization", "unified-overage-utilization"); var fallbackPct = GetHeader(headers, "anthropic-ratelimit-unified-fallback-percentage", "unified-fallback-percentage"); string plan; 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 = "valid", Plan = plan, IsSubscription = true }; 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; } 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; } } }