From 99914bd343ee5b06b15085a11e8f5c563162136a Mon Sep 17 00:00:00 2001 From: Meleki Date: Tue, 3 Mar 2026 09:14:04 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Stream-Error-Info=20f=C3=BCr=20Debuggin?= =?UTF-8?q?g=20bei=20unexpected=5Fend=20/=20max=5Ftokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chains Tabelle: neue Spalte stream_error_info (TEXT) - ChainRecord: neues Property StreamErrorInfo - AnthropicService/ProcessStream: sammelt Debug-Infos bei problematischen stop_reasons (unexpected_end, max_tokens) und API error-Events - FinalizeChain: speichert stream_error_info in DB - SSE done-Event: enthält streamErrorInfo für Client-seitiges Debugging --- Models/ChainRecord.cs | 1 + Services/AnthropicService.cs | 66 +++++++++++++++++++++++++++++++----- Services/DatabaseService.cs | 2 ++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/Models/ChainRecord.cs b/Models/ChainRecord.cs index 3b61ec5..9785a3c 100644 --- a/Models/ChainRecord.cs +++ b/Models/ChainRecord.cs @@ -18,5 +18,6 @@ public class ChainRecord public string ToolChoice { get; set; } public string ToolNames { get; set; } public string ArchiveId { get; set; } + public string StreamErrorInfo { get; set; } public string CreatedAt { get; set; } } diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 267876d..683b8df 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -119,7 +119,7 @@ public class AnthropicService return; } - var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace, agentId, cancellationToken); + var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, cancellationToken); lastStopReason = stopReason; messages.Add(new { role = "assistant", content = assistantContent }); @@ -169,7 +169,7 @@ public class AnthropicService if (stopReason != "tool_use") { var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason; - await FinalizeChain(chain, usage, iterations, finalStopReason, sw); + await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo); await TryWriteSse(response, "done", new { model = modelId, @@ -180,7 +180,8 @@ public class AnthropicService iterations, chainId, durationMs = sw.ElapsedMilliseconds, - stopReason = finalStopReason + stopReason = finalStopReason, + streamErrorInfo }, cancellationToken); return; } @@ -234,7 +235,7 @@ public class AnthropicService } } - private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw) + private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw, string streamErrorInfo = null) { chain.InputTokens = usage.InputTokens; chain.OutputTokens = usage.OutputTokens; @@ -242,11 +243,12 @@ public class AnthropicService chain.CacheReadTokens = usage.CacheReadTokens; chain.Iterations = iterations; chain.StopReason = stopReason; + chain.StreamErrorInfo = streamErrorInfo; chain.DurationMs = sw.ElapsedMilliseconds; await _db.UpdateChain(chain); } - private async Task<(string StopReason, List Content, List Tools)> ProcessStream( + private async Task<(string StopReason, List Content, List Tools, string StreamErrorInfo)> ProcessStream( HttpResponse response, HttpResponseMessage httpResp, TokenUsage usage, @@ -267,6 +269,7 @@ public class AnthropicService var blockTypes = new Dictionary(); var webSearchResultBlocks = new Dictionary(); var executedTools = new List(); + var streamErrors = new List(); string line; while ((line = await reader.ReadLineAsync(cancellationToken)) != null) @@ -433,21 +436,66 @@ public class AnthropicService case "message_delta": { - if (evt.TryGetProperty("delta", out var d) && - d.TryGetProperty("stop_reason", out var sr)) - stopReason = sr.GetString() ?? ""; + if (evt.TryGetProperty("delta", out var d)) + { + if (d.TryGetProperty("stop_reason", out var sr)) + { + stopReason = sr.GetString() ?? ""; + + // Bei problematischen stop_reasons Debug-Infos sammeln + if (stopReason is "unexpected_end" or "max_tokens") + { + var debugInfo = new Dictionary + { + ["event"] = "message_delta", + ["stop_reason"] = stopReason, + ["output_tokens"] = usage.OutputTokens, + ["text_length"] = currentTextBlock.Length, + ["timestamp"] = DateTime.UtcNow.ToString("o") + }; + if (d.TryGetProperty("stop_sequence", out var ss) && ss.ValueKind != JsonValueKind.Null) + debugInfo["stop_sequence"] = ss.GetString(); + streamErrors.Add(debugInfo); + } + } + } if (evt.TryGetProperty("usage", out var u2) && u2.TryGetProperty("output_tokens", out var ot)) usage.OutputTokens += ot.GetInt64(); break; } + + case "error": + { + // API Error-Event (z.B. overloaded_error) + var errorInfo = new Dictionary + { + ["event"] = "error", + ["timestamp"] = DateTime.UtcNow.ToString("o"), + ["raw"] = data + }; + if (evt.TryGetProperty("error", out var err)) + { + if (err.TryGetProperty("type", out var errType)) + errorInfo["error_type"] = errType.GetString(); + if (err.TryGetProperty("message", out var errMsg)) + errorInfo["error_message"] = errMsg.GetString(); + } + streamErrors.Add(errorInfo); + break; + } } } if (currentTextBlock.Length > 0) assistantContent.Add(new { type = "text", text = currentTextBlock.ToString() }); - return (stopReason, assistantContent, executedTools); + // Stream-Fehler als JSON serialisieren (null wenn keine Fehler) + string streamErrorInfo = streamErrors.Count > 0 + ? JsonSerializer.Serialize(streamErrors) + : null; + + return (stopReason, assistantContent, executedTools, streamErrorInfo); } private HttpClient CreateClient() diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 8c91e19..b96a0e1 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -140,6 +140,7 @@ public class DatabaseService conn.Execute("UPDATE agents SET auto_commit = 'enforce' WHERE auto_commit = '1' OR auto_commit = 1"); } catch { /* already migrated */ } + TryAddColumn(conn, "chains", "stream_error_info", "TEXT"); TryAddColumn(conn, "agents", "use_soul", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1"); TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1"); @@ -459,6 +460,7 @@ public class DatabaseService cache_read_tokens = @CacheReadTokens, iterations = @Iterations, stop_reason = @StopReason, + stream_error_info = @StreamErrorInfo, duration_ms = @DurationMs WHERE chain_id = @ChainId ", chain);