- AnthropicKeyChecker: Bearer-Auth + CLI-Header statt x-api-key (identisch mit AnthropicService) - 401 mit Usage-Headern → 'usage_limited' statt 'invalid' (Key gültig, Limit erreicht) - Haiku-Phase entfernt, Streaming-Logik entfernt → ein einfacher Request reicht - KeyScannerService: SearchLanguages 1→13, PathExtensions 24 neue Pfad-Filter - ApiKeysEndpoint: usage_limited als 'ok' ans Frontend - Frontend: Adopt-Button auch bei quota_exceeded, Live-Refresh alle 3s während Scan - CSS: Overflow-Ellipsis für lange URLs in Admin-Listen
134 lines
6.4 KiB
C#
134 lines
6.4 KiB
C#
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<KeyCheckResult> 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" };
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|