feat(frontend): display token usage per conversation turn (#1229)

Surface the usage_metadata that PR #1218 added to the streaming API.
A compact indicator in the chat header shows cumulative tokens consumed
per thread, with a tooltip breakdown of input/output/total counts.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Matt Van Horn
2026-03-23 17:59:35 -07:00
committed by GitHub
parent 8b0f3fe233
commit b40b05f623
7 changed files with 159 additions and 1 deletions

View File

@@ -274,6 +274,12 @@ export const enUS: Translations = {
failed: "Subtask failed",
},
// Token Usage
tokenUsage: {
title: "Token Usage",
input: "Input",
output: "Output",
total: "Total",
// Shortcuts
shortcuts: {
searchActions: "Search actions...",

View File

@@ -211,6 +211,12 @@ export interface Translations {
failed: string;
};
// Token Usage
tokenUsage: {
title: string;
input: string;
output: string;
total: string;
// Shortcuts
shortcuts: {
searchActions: string;

View File

@@ -261,6 +261,12 @@ export const zhCN: Translations = {
failed: "子任务失败",
},
// Token Usage
tokenUsage: {
title: "Token 用量",
input: "输入",
output: "输出",
total: "总计",
// Shortcuts
shortcuts: {
searchActions: "搜索操作...",

View File

@@ -0,0 +1,62 @@
import type { Message } from "@langchain/langgraph-sdk";
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
/**
* Extract usage_metadata from an AI message if present.
* The field is added by the backend (PR #1218) but not typed in the SDK.
*/
function getUsageMetadata(
message: Message,
): TokenUsage | null {
if (message.type !== "ai") {
return null;
}
const usage = (message as Record<string, unknown>).usage_metadata as
| { input_tokens?: number; output_tokens?: number; total_tokens?: number }
| undefined;
if (!usage) {
return null;
}
return {
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
totalTokens: usage.total_tokens ?? 0,
};
}
/**
* Accumulate token usage across all AI messages in a thread.
*/
export function accumulateUsage(messages: Message[]): TokenUsage | null {
const cumulative: TokenUsage = {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
};
let hasUsage = false;
for (const message of messages) {
const usage = getUsageMetadata(message);
if (usage) {
hasUsage = true;
cumulative.inputTokens += usage.inputTokens;
cumulative.outputTokens += usage.outputTokens;
cumulative.totalTokens += usage.totalTokens;
}
}
return hasUsage ? cumulative : null;
}
/**
* Format a token count for display: 1234 -> "1,234", 12345 -> "12.3K"
*/
export function formatTokenCount(count: number): string {
if (count < 10_000) {
return count.toLocaleString();
}
return `${(count / 1000).toFixed(1)}K`;
}