From 9391b5a6fa59ef53550bb336959f1b3f32135085 Mon Sep 17 00:00:00 2001 From: Meleki Date: Tue, 24 Mar 2026 16:52:31 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Verwaiste=20tool=5Fresult-Bl=C3=B6cke=20?= =?UTF-8?q?aus=20History=20entfernen=20(API=20400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wenn eine ältere ask_user_choice-Chain als Text-only geladen wird, geht der tool_use verloren, aber das tool_result in der nächsten Chain bleibt – Anthropic API antwortet mit 400 "unexpected tool_use_id". Neue FixOrphanedToolResults()-Funktion entfernt diese verwaisten Blöcke. Co-Authored-By: Claude Opus 4.6 --- Services/ChatService.cs | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 8a17f1f..39f5537 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -553,6 +553,11 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? // Post-Processing: Offene tool_use-Blöcke ohne tool_result → synthetische tool_results einfügen FixOrphanedToolUse(history); + // Post-Processing: tool_result-Blöcke ohne passendes tool_use entfernen + // Tritt auf wenn eine ältere ask_user_choice-Chain als Text-only geladen wird: + // Der tool_use(ask_user_choice) geht verloren, aber das tool_result(iter 999) in der nächsten Chain bleibt. + FixOrphanedToolResults(history); + // Post-Processing: Orphaned web_search server_tool_use entfernen // Bei alten DB-Einträgen wurde web_search_tool_result nur als Sources-Array gespeichert, // nicht als Raw-JSON → BuildHistoryFromDb überspringt sie (Array-Check) → @@ -656,6 +661,65 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? } } + /// + /// Entfernt verwaiste tool_result-Blöcke aus user-Messages: + /// tool_results deren tool_use_id in keinem vorherigen assistant-Block existiert. + /// Tritt auf wenn eine ältere ask_user_choice-Chain als Text-only geladen wird – + /// der tool_use geht verloren, aber das tool_result in der nächsten Chain bleibt. + /// + private static void FixOrphanedToolResults(List history) + { + // Alle tool_use IDs in der gesamten History sammeln + var allToolUseIds = new HashSet(); + foreach (var msg in history) + { + if (msg is not Dictionary dict) continue; + if (!dict.TryGetValue("role", out var role) || role?.ToString() != "assistant") continue; + if (!dict.TryGetValue("content", out var content) || content is not List contentList) continue; + + foreach (var block in contentList) + { + var json = JsonSerializer.Serialize(block); + JsonElement el; + try { el = JsonSerializer.Deserialize(json); } + catch { continue; } + + if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_use") continue; + if (el.TryGetProperty("id", out var idProp)) + { + var id = idProp.GetString(); + if (!string.IsNullOrEmpty(id)) + allToolUseIds.Add(id); + } + } + } + + // User-Messages durchgehen und orphaned tool_results entfernen + for (int i = history.Count - 1; i >= 0; i--) + { + if (history[i] is not Dictionary udict) continue; + if (!udict.TryGetValue("role", out var urole) || urole?.ToString() != "user") continue; + if (!udict.TryGetValue("content", out var ucontent) || ucontent is not List ucontentList) continue; + + ucontentList.RemoveAll(block => + { + var json = JsonSerializer.Serialize(block); + JsonElement el; + try { el = JsonSerializer.Deserialize(json); } + catch { return false; } + + if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_result") return false; + if (!el.TryGetProperty("tool_use_id", out var tid)) return false; + var toolUseId = tid.GetString(); + return !string.IsNullOrEmpty(toolUseId) && !allToolUseIds.Contains(toolUseId); + }); + + // Wenn die user-Message nach dem Entfernen leer ist, ganze Message entfernen + if (ucontentList.Count == 0) + history.RemoveAt(i); + } + } + /// /// Prüft die gesamte History auf tool_use-Blöcke ohne passendes tool_result in der nächsten user-Message. /// Falls gefunden, werden synthetische tool_results eingefügt – verhindert Anthropic API 400.