diff --git a/Endpoints/ModelsEndpoint.cs b/Endpoints/ModelsEndpoint.cs index 45013b6..2631642 100644 --- a/Endpoints/ModelsEndpoint.cs +++ b/Endpoints/ModelsEndpoint.cs @@ -16,6 +16,7 @@ public static class ModelsEndpoint app.MapGet("/api/models/gemini", HandleGeminiModels); app.MapGet("/api/models/openai", HandleOpenAiModels); app.MapGet("/api/models/minimax", HandleMinimaxModels); + app.MapGet("/api/models/glm", HandleGlmModels); app.MapPost("/api/models/refresh", HandleRefresh); } @@ -59,6 +60,14 @@ public static class ModelsEndpoint return Results.Json(new { models }); } + private static IResult HandleGlmModels(OpenAiService openAiService) + { + var models = openAiService.GetGlmModels() + .Select(m => new { id = m.Id, displayName = m.DisplayName }) + .ToList(); + return Results.Json(new { models }); + } + private static async Task HandleRefresh(OpenAiService openAiService) { await openAiService.RefreshModelsFromApis(); diff --git a/Program.cs b/Program.cs index 5b5b132..fd8744f 100644 --- a/Program.cs +++ b/Program.cs @@ -125,8 +125,9 @@ builder.Services.AddHostedService(sp => sp.GetRequiredService builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); -// Claude CLI Service (für OAuth-basierte Chats via CLI) -builder.Services.AddSingleton(); +// Claude CLI Service (für OAuth-basierte Chats via CLI; serverSettings für z.ai/GLM-Env) +builder.Services.AddSingleton(sp => + new ClaudeCliService(sp.GetRequiredService>(), serverSettings)); var app = builder.Build(); diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 932d6c0..3e63633 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -52,6 +52,12 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig // OpenAI-kompatible Provider werden unverändert durchgereicht (Format: "provider:model") if (OpenAiService.IsOpenAiProvider(model)) return model; + // z.ai / GLM laufen über den Anthropic-kompatiblen Claude-CLI-Pfad (NICHT über den + // OpenAI-Provider). Prefix strippen → die rohe Modell-ID (z.B. "glm-4.6") geht an --model, + // und das Routing erkennt Provider="" → CLI. Die z.ai-Env-Vars setzt ClaudeCliService. + if (!string.IsNullOrEmpty(model) && model.StartsWith("glm:", System.StringComparison.OrdinalIgnoreCase)) + return model.Substring(4); + // Defensiv-Strip: alte "[1m]"-Suffixe aus DB transparent normalisieren. // 1M-Context ist seit März 2026 Default bei Opus 4.6/4.7/4.8 + Sonnet 4.6 – kein Suffix mehr nötig. if (!string.IsNullOrEmpty(model) && model.Contains("[1m]")) diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 7c0a00f..50453dd 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -20,10 +20,12 @@ public class ClaudeCliService private readonly ILogger _logger; private readonly string _cliPath; private readonly bool _isAvailable; + private readonly ServerSettingsService _settings; - public ClaudeCliService(ILogger logger) + public ClaudeCliService(ILogger logger, ServerSettingsService settings) { _logger = logger; + _settings = settings; _cliPath = FindCliPath(); _isAvailable = !string.IsNullOrEmpty(_cliPath) && File.Exists(_cliPath); @@ -56,7 +58,14 @@ public class ClaudeCliService return dir; } - private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null, string workingDirectory = null) + /// + /// Erkennt z.ai/GLM-Modelle. Diese laufen über den Anthropic-kompatiblen z.ai-Endpoint, + /// nicht über das normale Claude-OAuth-Login. + /// + public static bool IsZaiModel(string modelId) + => !string.IsNullOrEmpty(modelId) && modelId.StartsWith("glm-", System.StringComparison.OrdinalIgnoreCase); + + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null) { var psi = new ProcessStartInfo { @@ -88,6 +97,32 @@ public class ClaudeCliService if (!string.IsNullOrEmpty(effort) && effort != "high") psi.Environment["CLAUDE_CODE_EFFORT_LEVEL"] = effort; + // z.ai / GLM: das Anthropic-kompatible z.ai-Backend NUR für diesen Prozess aktivieren. + // Da gott pro Request einen eigenen CLI-Prozess startet, gilt das ausschließlich für + // diesen Lauf – das Claude-OAuth-Login aller anderen Requests bleibt unberührt. + if (IsZaiModel(modelId)) + { + var zaiKey = _settings?.Get("ZaiApiKey"); + if (!string.IsNullOrEmpty(zaiKey)) + { + psi.Environment["ANTHROPIC_BASE_URL"] = "https://api.z.ai/api/anthropic"; + psi.Environment["ANTHROPIC_AUTH_TOKEN"] = zaiKey; + // Interne CLI-Hilfsaufrufe (z.B. Haiku-Klasse für Titel/Utility) auf GLM mappen, + // damit sie nicht gegen unbekannte Claude-Modellnamen bei z.ai laufen. + psi.Environment["ANTHROPIC_DEFAULT_SONNET_MODEL"] = modelId; + psi.Environment["ANTHROPIC_DEFAULT_OPUS_MODEL"] = modelId; + psi.Environment["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "glm-4.5-air"; + // Effort/Reasoning-Budget ist Claude-spezifisch und für z.ai nicht definiert → nicht senden. + psi.Environment.Remove("CLAUDE_CODE_EFFORT_LEVEL"); + // Ein evtl. global gesetzter Anthropic-API-Key darf den z.ai-Token nicht überschreiben. + psi.Environment.Remove("ANTHROPIC_API_KEY"); + } + else + { + _logger.LogWarning("GLM-Modell '{Model}' gewählt, aber ZaiApiKey nicht konfiguriert – Request läuft NICHT über z.ai.", modelId); + } + } + foreach (var arg in args) psi.ArgumentList.Add(arg); @@ -225,6 +260,11 @@ public class ClaudeCliService args.Add(tempImageDir); } + // Defensiv: falls eine unaufgelöste z.ai-ID ("glm:glm-4.6") durchrutscht, Prefix strippen, + // damit --model die rohe ID (z.B. "glm-4.6") erhält, die z.ai erwartet. + if (!string.IsNullOrEmpty(modelId) && modelId.StartsWith("glm:", System.StringComparison.OrdinalIgnoreCase)) + modelId = modelId.Substring(4); + if (!string.IsNullOrEmpty(modelId)) { args.Add("--model"); @@ -243,7 +283,7 @@ public class ClaudeCliService args.Add(mcpConfigPath); } - return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort, workspace); + return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort, workspace, modelId); } private async Task RunCliStreamingAsync( @@ -252,7 +292,8 @@ public class ClaudeCliService Func onEvent, CancellationToken cancellationToken, string effort = null, - string workspace = null) + string workspace = null, + string modelId = null) { var result = new CliChatResult(); var fullOutput = new StringBuilder(); @@ -272,7 +313,7 @@ public class ClaudeCliService try { - var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace); + var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace, modelId: modelId); using var process = new Process { StartInfo = startInfo }; diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index 6ed4f40..b7c17a0 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -71,6 +71,7 @@ public class OpenAiService private List _geminiModels = new List(); private List _openaiModels = new List(); private List _minimaxModels = new List(); + private List _glmModels = new List(); // Fallback-Modell-Liste für MiniMax (falls /v1/models nichts liefert). // Quelle: https://platform.minimax.io/docs/api-reference/api-overview @@ -126,6 +127,7 @@ public class OpenAiService _geminiModels = cache.ContainsKey("gemini") ? cache["gemini"] : new List(); _openaiModels = cache.ContainsKey("openai") ? cache["openai"] : new List(); _minimaxModels = cache.ContainsKey("minimax") ? cache["minimax"] : new List(); + _glmModels = cache.ContainsKey("glm") ? cache["glm"] : new List(); // Falls der Cache älter als die MiniMax-Integration ist oder leer ist: // statische Fallback-Liste verwenden, damit das Dropdown nicht leer bleibt. if (_minimaxModels.Count == 0) @@ -195,6 +197,13 @@ public class OpenAiService Console.WriteLine($"[OpenAiService] MinimaxApiKey nicht konfiguriert – nutze Fallback-Liste ({_minimaxModels.Count} Modelle)."); } + // z.ai / GLM: Key aus Settings (ZaiApiKey). OpenAI-kompatibler /models-Endpoint von z.ai. + var zaiKey = _settings.Get("ZaiApiKey"); + if (!string.IsNullOrEmpty(zaiKey)) + _glmModels = await FetchModels("https://api.z.ai/api/paas/v4/models", "GLM (z.ai)", zaiKey); + else + Console.WriteLine("[OpenAiService] ZaiApiKey nicht konfiguriert – GLM-Modelle nicht geladen."); + // Cache speichern try { @@ -204,7 +213,8 @@ public class OpenAiService ["pollinations"] = _pollinationsModels, ["gemini"] = _geminiModels, ["openai"] = _openaiModels, - ["minimax"] = _minimaxModels + ["minimax"] = _minimaxModels, + ["glm"] = _glmModels }; var json = JsonSerializer.Serialize(cache, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(_cacheFile, json); @@ -264,6 +274,7 @@ public class OpenAiService public List GetGeminiModels() => _geminiModels; public List GetOpenAiModels() => _openaiModels; public List GetMinimaxModels() => _minimaxModels; + public List GetGlmModels() => _glmModels; /// /// Gibt den Provider und das eigentliche Modell-ID aus einem kombinierten Model-String zurück. diff --git a/Services/ServerSettingsService.cs b/Services/ServerSettingsService.cs index 7c4f101..17bfd76 100644 --- a/Services/ServerSettingsService.cs +++ b/Services/ServerSettingsService.cs @@ -38,6 +38,7 @@ public class ServerSettingsService new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ), new SettingDefinition( "NvidiaApiKey", "string", "", "Nvidia API-Key (direkt statt g4f.space Proxy)", "password" ), new SettingDefinition( "MinimaxApiKey", "string", "", "MiniMax API-Key (für minimax-Provider im Chat)", "password" ), + new SettingDefinition( "ZaiApiKey", "string", "", "z.ai API-Key (für GLM-Modelle über die Claude CLI)", "password" ), new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ), }; diff --git a/wwwroot/index.html b/wwwroot/index.html index dfa54b1..fcee1e1 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -249,6 +249,9 @@ + + +