From 5418dc9225f7d791787a47050d9814cd1f3976e4 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sat, 20 Jun 2026 14:30:59 +0200 Subject: [PATCH] Enable GLM web search and 5.2+ effort over the z.ai CLI path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GLM models cannot run Anthropic's server-side WebSearch/WebFetch, so when a GLM model has web search enabled and ZaiApiKey is set, GenerateMcpConfig registers z.ai's web_search_prime HTTP MCP server and CreateCliStartInfo allows mcp__web-search-prime__* while disallowing the native WebSearch/WebFetch tools — otherwise the model tries the (headless-ungranted) native tool first and only falls back to the MCP search on the second attempt. Effort is now passed through for GLM-5.2 and newer (version compare, not a prefix match, so 5.3/6 are picked up automatically and glm-5.2[1m]/glm-5-turbo parse correctly); older GLM models still drop CLAUDE_CODE_EFFORT_LEVEL. Co-Authored-By: Claude Opus 4.8 --- Services/AnthropicService.Cli.cs | 25 ++++++++++++- Services/ClaudeCliService.cs | 64 +++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index dfefc60..ace7d37 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -73,7 +73,7 @@ public partial class AnthropicService var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null; var hasRemote = remoteConn != null; var remoteOs = remoteConn?.OsVersion ?? ""; - mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs); + mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs, modelId, webSearch); // Python-Verfügbarkeit prüfen und ggf. warnen if (FindPythonCommand() == null) @@ -903,7 +903,7 @@ public partial class AnthropicService /// Generiert eine temporäre MCP-Config-Datei für die Claude CLI. /// Enthält den MCP-Server für remote_execute. /// - private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null) + private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null, string modelId = null, bool webSearch = false) { // Port aus Konfiguration oder Default var port = 19000; @@ -963,6 +963,27 @@ public partial class AnthropicService mcpServers["unity"] = unityConfig; } + // z.ai Web-Search-Prime-MCP: GLM-Modelle können Anthropics native WebSearch nicht nutzen. + // Läuft ein GLM-Modell mit aktivierter Web-Suche und ist der z.ai-Key gesetzt, registrieren + // wir z.ais HTTP-MCP-Suchserver (Bearer-Auth mit demselben Key). Tool wird in den + // --allowed-tools der CLI (ClaudeCliService) als "mcp__web-search-prime__*" freigegeben. + if (webSearch && ClaudeCliService.IsZaiModel(modelId)) + { + var zaiKey = _settings?.Get("ZaiApiKey"); + if (!string.IsNullOrEmpty(zaiKey)) + { + mcpServers["web-search-prime"] = new Dictionary + { + ["type"] = "http", + ["url"] = "https://api.z.ai/api/mcp/web_search_prime/mcp", + ["headers"] = new Dictionary + { + ["Authorization"] = $"Bearer {zaiKey}" + } + }; + } + } + var config = new { mcpServers }; var json = System.Text.Json.JsonSerializer.Serialize(config, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 50453dd..29e60a6 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -65,6 +65,31 @@ public class ClaudeCliService public static bool IsZaiModel(string modelId) => !string.IsNullOrEmpty(modelId) && modelId.StartsWith("glm-", System.StringComparison.OrdinalIgnoreCase); + /// + /// GLM-5.2 (und höher) hat (laut z.ai-Doku) ein Reasoning-Effort-Mapping: + /// Claude Code mappt low/medium/high → GLM-"high", xhigh/max → GLM-"max". Ältere GLM-Modelle + /// (4.x, 5, 5-turbo, 5.1) kennen kein Effort. Bewusst ein Versionsvergleich statt Prefix-Match, + /// damit künftige Releases (5.3, 6, …) automatisch mitgenommen werden. Erfasst auch + /// Varianten-Suffixe wie "glm-5.2[1m]" oder "glm-5-turbo". + /// + private static bool IsZaiEffortModel(string modelId) + { + if (string.IsNullOrEmpty(modelId) || !modelId.StartsWith("glm-", System.StringComparison.OrdinalIgnoreCase)) + return false; + // Führende Versionsziffern nach "glm-" extrahieren: "5.2[1m]" → 5.2, "5-turbo" → 5(.0). + var rest = modelId.Substring(4); + int i = 0; + while (i < rest.Length && (char.IsDigit(rest[i]) || rest[i] == '.')) i++; + var versionStr = rest.Substring(0, i).Trim('.'); + if (versionStr.Length == 0) return false; + var parts = versionStr.Split('.'); + if (!int.TryParse(parts[0], out var major)) return false; + var minor = 0; + if (parts.Length > 1) int.TryParse(parts[1], out minor); + // GLM-5.2 oder höher. + return major > 5 || (major == 5 && minor >= 2); + } + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null) { var psi = new ProcessStartInfo @@ -112,8 +137,12 @@ public class ClaudeCliService 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"); + // Effort/Reasoning: NUR GLM-5.2 hat ein definiertes Effort-Mapping bei z.ai + // (Claude Code mappt es selbst: low/medium/high → "high", xhigh/max → "max"). + // Für 5.2 reichen wir CLAUDE_CODE_EFFORT_LEVEL daher durch; alle anderen GLM-Modelle + // kennen kein Effort → Env-Var entfernen, damit nichts Undefiniertes gesendet wird. + if (!IsZaiEffortModel(modelId)) + 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"); } @@ -207,10 +236,12 @@ public class ClaudeCliService "--output-format", "stream-json", "--include-partial-messages", "--no-session-persistence", - "--permission-mode", permissionMode, - "--disallowed-tools", "AskUserQuestion" + "--permission-mode", permissionMode }; + // Tools, die das Modell gar nicht erst anbieten/versuchen soll. + var disallowedTools = new List { "AskUserQuestion" }; + // Tools konfigurieren var tools = new List(); if (hasWorkspace && planMode) @@ -228,7 +259,27 @@ public class ClaudeCliService // Kein Workspace: Read für Temp-Dateien (z.B. gesendete Bilder) tools.Add("Read"); } - if (webSearch) { tools.Add("WebFetch"); tools.Add("WebSearch"); } + if (webSearch) + { + // GLM/z.ai kann Anthropics server-seitige WebSearch/WebFetch nicht ausführen → + // stattdessen z.ais Web-Search-Prime-MCP-Tool zulassen. Der zugehörige MCP-Server + // wird in GenerateMcpConfig injiziert, sobald GLM + Web-Suche + ZaiApiKey gesetzt sind. + if (IsZaiModel(modelId) && !string.IsNullOrEmpty(_settings?.Get("ZaiApiKey"))) + { + tools.Add("mcp__web-search-prime__*"); + // Die nativen Web-Tools komplett verbieten: sonst versucht das Modell zuerst + // eine (über z.ai nicht ausführbare) WebSearch, scheitert headless an der + // Berechtigung und greift erst im zweiten Anlauf zum Prime-MCP. Mit dem Verbot + // werden sie dem Modell gar nicht angeboten → direkt das Prime-Tool. + disallowedTools.Add("WebSearch"); + disallowedTools.Add("WebFetch"); + } + else + { + tools.Add("WebFetch"); + tools.Add("WebSearch"); + } + } // MCP-Tools erlauben (wenn MCP-Config vorhanden) if (!string.IsNullOrEmpty(mcpConfigPath)) { @@ -246,6 +297,9 @@ public class ClaudeCliService args.Add("--allowed-tools"); args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\""); + args.Add("--disallowed-tools"); + args.Add(string.Join(" ", disallowedTools)); + if (!string.IsNullOrEmpty(workspace)) { args.Add("--add-dir");