feat(coach): surface per-turn token usage on the chat stream

Carry an additive usage envelope on the SSE done frame
({ totalTokens, model }) and persist tokensUsed + model onto
coach_messages (migration 0182, nullable, no backfill) so the
per-message token footer survives a conversation reload. Add an
optional reasoning frame the client can render inside the thinking
disclosure when a reasoning-capable provider emits a summary; providers
without reasoning emit none. Extend CoachMessageDTO and regenerate the
OpenAPI contract. Server-authoritative: clients display the count, never
recompute it.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 11:58:58 +02:00
1 parent a5b3715a46
commit f46e9d3bb8
9 files changed
+242 -29

No files matched your search

+13
View File
@@ -652,6 +652,11 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
metricSource: enrichedProvenance,
providerType: workingProviderType,
promptVersion: PROMPT_VERSION,
// v1.18.9 — persist the per-turn token count + model so the quiet
// token footer survives a conversation reload. The live turn paints
// from the `done.usage` SSE frame below; reloads read these columns.
tokensUsed: result.tokensUsed ?? null,
model: result.model ?? null,
});
// v1.18.7 — the day's spend was already reconciled against the
@@ -703,11 +708,19 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
encodeFrame({ type: "suggestion", suggestion: surfacedSuggestion }),
);
}
// v1.18.9 — additive `usage` envelope on the `done` frame so the client
// can paint the quiet per-message token footer the instant the stream
// closes. Server-authoritative: the client renders these numbers, never
// recomputes them. Older clients drop the unknown key.
controller.enqueue(
encodeFrame({
type: "done",
conversationId: workingConversationId,
messageId: assistantMessage.id,
usage: {
totalTokens: result.tokensUsed ?? null,
model: result.model ?? null,
},
}),
);
});