Files
gott/Services/AnthropicKeyChecker.cs
MelekiandClaude Opus 4.6 92ecaf9dac fix: ask_user_choice Short-Poll, Freitext-Option, Stop-Kontext-Fix
- Long-Poll durch Short-Poll ersetzt (MCP-Server pollt jede Sekunde statt einer blockierenden HTTP-Anfrage)
- Expliziter Lambda-Rückgabetyp Task<IResult> für ASP.NET Minimal API Endpoints (behebt 200 empty body Bug)
- Freitext-Option bei Choice-Buttons ("Eigene Antwort…") für freie User-Eingabe
- Stop während ask_user_choice markiert Chain als awaiting_user_choice statt stopped (Kontext bleibt erhalten)
- ask_user_choice System-Prompt-Instruktion breiter formuliert (Quiz, Multiple-Choice etc.)
- Text wird jetzt immer in DB gespeichert, auch bei ask_user_choice
- Key-Check Model auf claude-haiku-4-5 geändert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 11:32:25 +01:00

191 lines
9.6 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-haiku-4-5"; //"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");
var isOAuth = apiKey.StartsWith("sk-ant-oat01-", StringComparison.OrdinalIgnoreCase);
if (isOAuth)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
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");
}
else
{
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;
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)
{
var body400 = await resp.Content.ReadAsStringAsync();
if (body400.Contains("credit balance", StringComparison.OrdinalIgnoreCase))
return new KeyCheckResult { Status = "billing_not_active", Message = "Gültig, aber kein Guthaben (credit balance too low)", Plan = "API Console" };
if (body400.Contains("billing", StringComparison.OrdinalIgnoreCase) || body400.Contains("payment", StringComparison.OrdinalIgnoreCase))
return new KeyCheckResult { Status = "billing_not_active", Message = "Gültig, aber Billing-Problem (400)", Plan = "API Console" };
return new KeyCheckResult { Status = "error", Message = $"API-Fehler (400): {(body400.Length > 200 ? body400.Substring(0, 200) : body400)}", 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" };
}
}
/// <summary>
/// Extrahiert Plan/Usage-Metadaten nur aus Response-Headern (kein eigener HTTP-Request).
/// Kann null zurückgeben wenn keine Usage-Header vorhanden sind.
/// </summary>
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;
}
}
}