feat: Stream-Error-Info für Debugging bei unexpected_end / max_tokens

- 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
This commit is contained in:
Meleki committed 2026-03-03 09:14:04 +01:00
1 parent dc87bf802a
commit 99914bd343
3 files changed
+60 -9

No files matched your search

+1
View File
@@ -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; }
}
+57 -9
View File
@@ -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<object> Content, List<ToolResult> Tools)> ProcessStream(
private async Task<(string StopReason, List<object> Content, List<ToolResult> Tools, string StreamErrorInfo)> ProcessStream(
HttpResponse response,
HttpResponseMessage httpResp,
TokenUsage usage,
@@ -267,6 +269,7 @@ public class AnthropicService
var blockTypes = new Dictionary<int, string>();
var webSearchResultBlocks = new Dictionary<int, object>();
var executedTools = new List<ToolResult>();
var streamErrors = new List<object>();
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<string, object>
{
["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<string, object>
{
["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()
+2
View File
@@ -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);