Files
gott/Services/SseHelper.cs
T
MelekiandClaude Opus 4.8 47557c9724 Include panel member tokens in the chain cost badge
Panel member token usage was tracked but discarded, so the cost badge only
ever showed the worker's tokens — misleading for MoA where the real cost is
roughly rounds x agents. Collect each member's usage and add it to the badge.

Each member fills its own TokenUsage in parallel (Anthropic loop via the
existing memberUsage, CLI via the now-captured CliChatResult return value;
OpenAI members stay null, nachruestbar). RunPanel sums them after the
Task.WhenAll barrier — single-threaded, so no lock or Interlocked needed —
across all MoA rounds and emits a panel_usage SSE event before panel_done.
The frontend adds those tokens onto the worker totals in the done handler so
the badge reflects the full chain cost.

- SseHelper: add TokenUsage.Add()
- AnthropicService.Panel.cs: PanelMemberResult.Usage, accumulate per round
  after the barrier, emit panel_usage; RunPanelMemberViaCli returns usage
- chat.js: _panelUsage state, panel_usage case, add to worker tokens on done,
  reset on stream start
- TODO.md: check off the member-token item

v1 limit: live badge only — FinalizeChain still persists worker tokens, so a
reload shows worker-only cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 18:08:55 +02:00

114 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Models;
using Microsoft.AspNetCore.Http;
namespace Gott.Services;
/// <summary>
/// Shared SSE-Streaming-Utilities für AnthropicService und OpenAiService.
/// </summary>
public static class SseHelper
{
/// <summary>
/// Schreibt ein SSE-Event (wirft bei CancellationToken).
/// </summary>
public static async Task WriteSseEvent(HttpResponse response, string eventType, object data, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var json = JsonSerializer.Serialize(data);
await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n", ct);
await response.Body.FlushAsync(ct);
}
/// <summary>
/// Versucht ein SSE-Event zu schreiben, ignoriert Fehler (Client disconnected).
/// Optionaler <paramref name="gate"/>: serialisiert die Byte-Writes, wenn mehrere
/// Producer (z.B. parallele Panel-Member) auf DIESELBE HttpResponse schreiben –
/// sonst vermischen sich die SSE-Frames auf Byte-Ebene.
/// </summary>
public static async Task TryWriteSse(HttpResponse response, string eventType, object data, CancellationToken ct, SemaphoreSlim gate = null)
{
if (ct.IsCancellationRequested) return;
bool acquired = false;
try
{
if (gate != null)
{
await gate.WaitAsync(ct);
acquired = true;
}
await WriteSseEvent(response, eventType, data, ct);
}
catch
{
// Client bereits disconnected / abgebrochen – ignorieren
}
finally
{
if (acquired) gate.Release();
}
}
/// <summary>
/// Finalisiert eine Chain in der Datenbank (Token-Counts, Stopgrund, Dauer).
/// </summary>
public static async Task FinalizeChain(DatabaseService db, ChainRecord chain, TokenUsage usage, int iterations, string stopReason, Stopwatch sw, string streamErrorInfo = null)
{
chain.InputTokens = usage.InputTokens;
chain.OutputTokens = usage.OutputTokens;
chain.CacheCreationTokens = usage.CacheCreationTokens;
chain.CacheReadTokens = usage.CacheReadTokens;
chain.Iterations = iterations;
chain.StopReason = stopReason;
chain.StreamErrorInfo = streamErrorInfo;
chain.DurationMs = sw.ElapsedMilliseconds;
await db.UpdateChain(chain);
}
/// <summary>
/// Kürzt Tool-Results auf max. Zeichenzahl.
/// </summary>
public const int MaxToolResultChars = 20_000;
public static string TruncateToolResult(string result)
{
return result.Length > MaxToolResultChars
? result[..MaxToolResultChars] + $"\n\n[... gekürzt, {result.Length - MaxToolResultChars} weitere Zeichen ...]"
: result;
}
}
/// <summary>
/// Token-Zähler für eine Chat-Session (Input, Output, Cache).
/// </summary>
public class TokenUsage
{
public long InputTokens { get; set; }
public long OutputTokens { get; set; }
public long CacheCreationTokens { get; set; }
public long CacheReadTokens { get; set; }
/// <summary>
/// Addiert die Werte von <paramref name="other"/> auf. NICHT thread-safe – der einzige
/// Aufrufer (Panel-Engine) summiert die Member-Usage erst NACH der Task.WhenAll-Barrier,
/// also single-threaded. Jeder Member befüllt seine EIGENE TokenUsage parallel; geteilt
/// wird nichts → kein Lock/Interlocked nötig.
/// </summary>
public void Add(TokenUsage other)
{
if (other == null) return;
InputTokens += other.InputTokens;
OutputTokens += other.OutputTokens;
CacheCreationTokens += other.CacheCreationTokens;
CacheReadTokens += other.CacheReadTokens;
}
}
/// <summary>
/// Ergebnis einer Tool-Ausführung.
/// </summary>
public record ToolResult(string Id, string Result, bool IsError);