Persist panel member token costs to DB via usage seed

Replace the separate panel_usage SSE event + frontend accumulator with
server-side seeding of the worker's usage object. Both the done-event and
FinalizeChain read from the same usage, so panel (Fusion/MoA) member tokens
now flow into the live badge AND the DB as a single source of truth — the
badge no longer drops to the worker-only value after F5.

- RunPanel takes a TokenUsage usageSink; accumulates member costs after the
  Task.WhenAll barrier (single-threaded, no lock)
- ChatEndpoint passes panelUsage as seedUsage to all three worker paths
- StreamChat / StreamChatViaCli / OpenAI StreamChat fold seedUsage into usage
  at creation time, so every exit path persists it
- OpenAI done-events extended with cacheCreation/cacheRead fields for live
  consistency when seeded by Anthropic members
- chat.js: remove redundant _panelUsage / _addPanelUsage / panel_usage
- TODO.md: mark the live-only v1 boundary closed, note the new mechanism

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-19 08:44:36 +02:00
1 parent 1b203707ec
commit 5c89adb6df
7 files changed
+41 -46

No files matched your search

+10 -4
View File
@@ -236,13 +236,19 @@ public static class ChatEndpoint
// danach als Synthesizer über den unveränderten Dispatch unten. Heartbeat läuft bereits,
// Cancellation (workCt/sseCt) ist verfügbar. Nur bei aktivem Fusion-Modus + nicht-leerem
// Panel + keiner Choice-Antwort.
//
// Panel-Token-Kosten (alle Member über alle Runden) werden in panelUsage gesammelt und
// unten als seedUsage in die Worker-Streaming-Methode gefaltet → fließen über das
// done-Event (Live-Badge) UND FinalizeChain (DB) als Teil der Chain-Kosten und überleben
// F5. Bleibt {0} wenn kein Panel läuft → Seeding ist dann ein No-op.
var panelUsage = new TokenUsage();
if (!isChoiceResponse && (ctx.MultiModelMode == "fusion" || ctx.MultiModelMode == "moa") && ctx.PanelAgents != null && ctx.PanelAgents.Count > 0)
{
try
{
// Fusion = 1 Runde, MoA = MoaLayers Proposer-Runden vor der Synthese.
var panelLayers = ctx.MultiModelMode == "moa" ? System.Math.Max(1, ctx.MoaLayers) : 1;
await anthropic.RunPanel(res, ctx.PanelAgents, messages, ctx.ChainId, sessionId, userApiKey?.ApiKey, panelLayers, workCt, sseCt);
await anthropic.RunPanel(res, ctx.PanelAgents, messages, ctx.ChainId, sessionId, userApiKey?.ApiKey, panelLayers, panelUsage, workCt, sseCt);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
@@ -265,7 +271,7 @@ public static class ChatEndpoint
return;
}
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, maxIterations: ctx.MaxIterations, apiKeyOverride: openAiKeyOverride, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, sseCt: sseCt);
await openAi.StreamChat(res, ctx.ModelId, ctx.SystemPromptText, messages, ctx.ChainId, workspace: ctx.Workspace, sessionId: sessionId, agentId: ctx.AgentId, maxIterations: ctx.MaxIterations, apiKeyOverride: openAiKeyOverride, useAutoContext: ctx.Agent?.UseAutoContext ?? true, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt);
}
else
{
@@ -286,7 +292,7 @@ public static class ChatEndpoint
if (anthropic.IsCliAvailable)
{
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, workCt: workCt, sseCt: sseCt);
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt);
}
else
{
@@ -296,7 +302,7 @@ public static class ChatEndpoint
return;
}
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, effort: ctx.Effort, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
}
}
finally