feat(server): persist and display assistant message token usage #334

Merged
rwachters merged 3 commits from feat/assistant-message-usage-stats into master 2026-10-01 16:49:38 +00:00
rwachters commented 2026-10-01 16:48:13 +00:00 (Migrated from github.com)

Summary

Persists the token usage a provider reports for an assistant generation and surfaces it to the user from the message header, so token consumption is auditable per message instead of being discarded after each stream.

Changes

  • Added a shared serializable UsageStats and an optional usageStats field on ChatMessage.AssistantMessage, with null meaning "no usage reported" rather than zero tokens.
  • Renamed the LLM token counters to inputTokens/outputTokens and replaced the nested LLMCompletionResult.UsageStats with the shared model.
  • Added LLMStreamChunk.FinalUsageStats, emitted by the OpenAI, Responses and Ollama dialects immediately before their terminal chunk, so usage is recorded from one authoritative signal.
  • Renamed and nullable-ised the OpenAI-compatible usage counters so an omitted counter stays absent instead of being zero-filled.
  • Persisted usage through the V33__assistant_message_usage.sql migration, the Exposed column, a tolerant mapper decode, DAO insert/update plumbing, turn persistence and the session clone copy.
  • Added an app-side message details dialog reached from a clickable <model>: header on finalized assistant messages, with English and Spanish strings.
  • Extracted a shared formatInstant helper now used by both the usage dialog and the file reference dialog.

Implementation notes

  • Usage is only written from a provider-reported value, matching the "no counter reported ⇒ no usage" rule; a generation that does not end normally carries no usage.
  • The streamed UsageChunk observation path is retained but is no longer emitted by any dialect; FinalUsageStats is the single terminal source of truth.
  • V33 is used because V32 is already taken by a Kotlin BaseJavaMigration; the migration is additive and nullable, so no data backfill is required.
  • updateMessageContent writes usage unconditionally, which matches its only two callers (turn finalization and the public edit path).

Validation

  • ./gradlew :server:classes
  • ./gradlew :server:testClasses
  • ./gradlew :server:test
  • ./gradlew :app:desktopMainClasses
  • ./gradlew :app:desktopTestClasses
  • ./gradlew :app:desktopTest
  • ./gradlew :common:desktopTest

The implementer ran all of the above with BUILD SUCCESSFUL; the independent review re-ran :server:test and :app:desktopTest (2164 / 945 tests, 0 failures). ./gradlew build was intentionally not run.

Risks and follow-up

  • The LOW-5/LOW-6 remediation was verified by the author only, without a separate independent verification pass.
  • Coverage for a few acceptance criteria (AC-8, AC-9, AC-10) remains partial; recorded as low-priority follow-ups, not blockers.
  • AssistantMessage now emits usageStats: null on the wire for messages without usage, so older strict clients must tolerate the extra property.
## Summary Persists the token usage a provider reports for an assistant generation and surfaces it to the user from the message header, so token consumption is auditable per message instead of being discarded after each stream. ## Changes - Added a shared serializable `UsageStats` and an optional `usageStats` field on `ChatMessage.AssistantMessage`, with `null` meaning "no usage reported" rather than zero tokens. - Renamed the LLM token counters to `inputTokens`/`outputTokens` and replaced the nested `LLMCompletionResult.UsageStats` with the shared model. - Added `LLMStreamChunk.FinalUsageStats`, emitted by the OpenAI, Responses and Ollama dialects immediately before their terminal chunk, so usage is recorded from one authoritative signal. - Renamed and nullable-ised the OpenAI-compatible usage counters so an omitted counter stays absent instead of being zero-filled. - Persisted usage through the `V33__assistant_message_usage.sql` migration, the Exposed column, a tolerant mapper decode, DAO insert/update plumbing, turn persistence and the session clone copy. - Added an app-side message details dialog reached from a clickable `<model>:` header on finalized assistant messages, with English and Spanish strings. - Extracted a shared `formatInstant` helper now used by both the usage dialog and the file reference dialog. ## Implementation notes - Usage is only written from a provider-reported value, matching the "no counter reported ⇒ no usage" rule; a generation that does not end normally carries no usage. - The streamed `UsageChunk` observation path is retained but is no longer emitted by any dialect; `FinalUsageStats` is the single terminal source of truth. - `V33` is used because `V32` is already taken by a Kotlin `BaseJavaMigration`; the migration is additive and nullable, so no data backfill is required. - `updateMessageContent` writes usage unconditionally, which matches its only two callers (turn finalization and the public edit path). ## Validation - `./gradlew :server:classes` - `./gradlew :server:testClasses` - `./gradlew :server:test` - `./gradlew :app:desktopMainClasses` - `./gradlew :app:desktopTestClasses` - `./gradlew :app:desktopTest` - `./gradlew :common:desktopTest` The implementer ran all of the above with `BUILD SUCCESSFUL`; the independent review re-ran `:server:test` and `:app:desktopTest` (2164 / 945 tests, 0 failures). `./gradlew build` was intentionally not run. ## Risks and follow-up - The LOW-5/LOW-6 remediation was verified by the author only, without a separate independent verification pass. - Coverage for a few acceptance criteria (AC-8, AC-9, AC-10) remains partial; recorded as low-priority follow-ups, not blockers. - `AssistantMessage` now emits `usageStats: null` on the wire for messages without usage, so older strict clients must tolerate the extra property.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!334
No description provided.