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:
1 parent
dc87bf802a
commit
99914bd343
3 files changed
+60
-9
No files matched your search
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user