From 15d353e6e60afa20c2380541183f5fb26610da8e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Jan 2026 14:53:11 +0000 Subject: [PATCH] feat: add cost transparency for AI chat messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-message cost display: - Add model, usage, cost fields to AssistantMessage (local-only, not sent to API) - Store cost/model/usage when saving assistant messages in session-manager - Display subtle cost footer on assistant messages: "gpt-4o-mini • 847 tokens • $0.0012" - Clean model names by removing provider prefixes and date suffixes Session cost display: - Calculate total conversation cost from messages - Show "Session: $0.0234" above input area when cost > 0 - Updates automatically as messages are added Cost formatting: - Show 4 decimal places for costs < $0.01 - Show 2 decimal places for costs >= $0.01 - Show "<$0.0001" for very small costs https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC --- src/components/AIViewer.tsx | 197 +++++++++++++++++++--------- src/services/llm/session-manager.ts | 26 ++-- src/types/llm.ts | 11 ++ 3 files changed, 159 insertions(+), 75 deletions(-) diff --git a/src/components/AIViewer.tsx b/src/components/AIViewer.tsx index c4bc2f5..56eb1b2 100644 --- a/src/components/AIViewer.tsx +++ b/src/components/AIViewer.tsx @@ -6,7 +6,7 @@ * Powered by ChatSessionManager for multi-window support. */ -import { useState, useEffect, useCallback, useRef, memo } from "react"; +import { useState, useEffect, useCallback, useRef, memo, useMemo } from "react"; import { Loader2, PanelLeft, @@ -17,7 +17,6 @@ import { Brain, Settings2, MessageSquare, - RefreshCw, Play, Sparkles, AlertCircle, @@ -121,46 +120,58 @@ const MessageBubble = memo(function MessageBubble({ // Assistant message const assistantMsg = message as AssistantMessage; + // Format cost info for display + const costInfo = formatMessageCost(assistantMsg); + return (
-
- {/* Reasoning content (collapsible) */} - {assistantMsg.reasoning_content && ( -
- - - Reasoning - -
- {assistantMsg.reasoning_content} -
-
- )} - - {/* Tool calls */} - {assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0 && ( -
- {assistantMsg.tool_calls.map((tc) => ( -
-
- - {tc.function.name} -
-
-                  {formatToolArgs(tc.function.arguments)}
-                
+
+
+ {/* Reasoning content (collapsible) */} + {assistantMsg.reasoning_content && ( +
+ + + Reasoning + +
+ {assistantMsg.reasoning_content}
- ))} -
- )} + + )} - {/* Regular content */} - {content && ( -
- + {/* Tool calls */} + {assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0 && ( +
+ {assistantMsg.tool_calls.map((tc) => ( +
+
+ + {tc.function.name} +
+
+                    {formatToolArgs(tc.function.arguments)}
+                  
+
+ ))} +
+ )} + + {/* Regular content */} + {content && ( +
+ +
+ )} +
+ + {/* Cost info footer */} + {costInfo && ( +
+ {costInfo}
)}
@@ -180,6 +191,48 @@ function formatToolArgs(args: string): string { } } +/** + * Format message cost info for display. + * Returns null if no cost info available. + */ +function formatMessageCost(msg: AssistantMessage): string | null { + const parts: string[] = []; + + // Model name (clean it up for display) + if (msg.model) { + const modelName = msg.model + .replace(/^(openai\/|anthropic\/|google\/|meta-llama\/|mistralai\/)/, "") + .replace(/-\d{4}-\d{2}-\d{2}$/, ""); + parts.push(modelName); + } + + // Token count + if (msg.usage) { + const total = msg.usage.promptTokens + msg.usage.completionTokens; + parts.push(`${total.toLocaleString()} tokens`); + } + + // Cost + if (msg.cost !== undefined && msg.cost > 0) { + parts.push(formatCost(msg.cost)); + } + + return parts.length > 0 ? parts.join(" • ") : null; +} + +/** + * Format cost in USD. + */ +function formatCost(cost: number): string { + if (cost < 0.0001) { + return "<$0.0001"; + } + if (cost < 0.01) { + return `$${cost.toFixed(4)}`; + } + return `$${cost.toFixed(2)}`; +} + // ───────────────────────────────────────────────────────────── // Thinking Indicator // ───────────────────────────────────────────────────────────── @@ -273,6 +326,16 @@ function ChatPanel({ // Prompt options for selector const promptOptions = usePromptOptions(); + // Calculate total conversation cost from messages + const conversationCost = useMemo(() => { + return messages.reduce((total, msg) => { + if (msg.role === "assistant" && "cost" in msg && msg.cost) { + return total + msg.cost; + } + return total; + }, 0); + }, [messages]); + // Local UI state const [input, setInput] = useState(""); const [pendingUserMessage, setPendingUserMessage] = useState( @@ -499,27 +562,37 @@ function ChatPanel({
-
-
-