Skip to content

Commit

Permalink
feat: add token usage tracking and message actions (#116)
Browse files Browse the repository at this point in the history
* feat: add token usage tracking and message actions

- Add token usage tracking for chat messages
- Implement message actions UI with copy and token info
- Add pricing constants for different LLM models
  • Loading branch information
kevin-on authored Nov 21, 2024
1 parent ff0fbef commit 72402a9
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 22 deletions.
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@lexical/react": "^0.17.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.56.2",
"clsx": "^2.1.1",
Expand Down
90 changes: 90 additions & 0 deletions src/components/chat-view/AssistantMessageActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { Check, CopyIcon } from 'lucide-react'
import { useMemo, useState } from 'react'

import { ChatAssistantMessage } from '../../types/chat'
import { calculateLLMCost } from '../../utils/price-calculator'

import LLMResponseInfoPopover from './LLMResponseInfoPopover'

function CopyButton({ message }: { message: ChatAssistantMessage }) {
const [copied, setCopied] = useState(false)

const handleCopy = async () => {
await navigator.clipboard.writeText(message.content)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1500)
}

return (
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button>
{copied ? (
<Check
size={12}
className="smtcmp-assistant-message-actions-icon--copied"
/>
) : (
<CopyIcon onClick={handleCopy} size={12} />
)}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="smtcmp-tooltip-content">
Copy message
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
)
}

function LLMResponesInfoButton({ message }: { message: ChatAssistantMessage }) {
const cost = useMemo<number | null>(() => {
if (!message.metadata?.model || !message.metadata?.usage) {
return 0
}
return calculateLLMCost({
model: message.metadata.model,
usage: message.metadata.usage,
})
}, [message])

return (
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div>
<LLMResponseInfoPopover
usage={message.metadata?.usage}
estimatedPrice={cost}
model={message.metadata?.model?.model}
/>
</div>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="smtcmp-tooltip-content">
View details
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
)
}

export default function AssistantMessageActions({
message,
}: {
message: ChatAssistantMessage
}) {
return (
<div className="smtcmp-assistant-message-actions">
<LLMResponesInfoButton message={message} />
<CopyButton message={message} />
</div>
)
}
51 changes: 39 additions & 12 deletions src/components/chat-view/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { readTFileContent } from '../../utils/obsidian'
import { openSettingsModalWithError } from '../../utils/openSettingsModal'
import { PromptGenerator } from '../../utils/promptGenerator'

import AssistantMessageActions from './AssistantMessageActions'
import ChatUserInput, { ChatUserInputRef } from './chat-input/ChatUserInput'
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
import { ChatListDropdown } from './ChatListDropdown'
Expand Down Expand Up @@ -237,7 +238,15 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
const responseMessageId = uuidv4()
setChatMessages([
...newChatHistory,
{ role: 'assistant', content: '', id: responseMessageId },
{
role: 'assistant',
content: '',
id: responseMessageId,
metadata: {
usage: undefined,
model: undefined,
},
},
])

try {
Expand All @@ -256,7 +265,15 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {

setChatMessages([
...compiledMessages,
{ role: 'assistant', content: '', id: responseMessageId },
{
role: 'assistant',
content: '',
id: responseMessageId,
metadata: {
usage: undefined,
model: undefined,
},
},
])
const stream = await streamResponse(
chatModel,
Expand All @@ -275,7 +292,15 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
setChatMessages((prevChatHistory) =>
prevChatHistory.map((message) =>
message.role === 'assistant' && message.id === responseMessageId
? { ...message, content: message.content + content }
? {
...message,
content: message.content + content,
metadata: {
...message.metadata,
usage: chunk.usage ?? message.metadata?.usage, // Keep existing usage if chunk has no usage data
model: chatModel,
},
}
: message,
),
)
Expand Down Expand Up @@ -596,15 +621,17 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
)}
</div>
) : (
<ReactMarkdownItem
key={message.id}
index={index}
chatMessages={chatMessages}
handleApply={handleApply}
isApplying={applyMutation.isPending}
>
{message.content}
</ReactMarkdownItem>
<div key={message.id} className="smtcmp-chat-messages-assistant">
<ReactMarkdownItem
index={index}
chatMessages={chatMessages}
handleApply={handleApply}
isApplying={applyMutation.isPending}
>
{message.content}
</ReactMarkdownItem>
{message.content && <AssistantMessageActions message={message} />}
</div>
),
)}
<QueryProgress state={queryProgress} />
Expand Down
84 changes: 84 additions & 0 deletions src/components/chat-view/LLMResponseInfoPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as Popover from '@radix-ui/react-popover'
import {
ArrowDown,
ArrowRightLeft,
ArrowUp,
Coins,
Cpu,
Info,
} from 'lucide-react'

import { ResponseUsage } from '../../types/llm/response'

type LLMResponseInfoProps = {
usage?: ResponseUsage
estimatedPrice: number | null
model?: string
}

export default function LLMResponseInfoPopover({
usage,
estimatedPrice,
model,
}: LLMResponseInfoProps) {
return (
<Popover.Root>
<Popover.Trigger asChild>
<button>
<Info className="smtcmp-llm-info-icon--trigger" size={12} />
</button>
</Popover.Trigger>
{usage ? (
<Popover.Content className="smtcmp-popover-content smtcmp-llm-info-content">
<div className="smtcmp-llm-info-header">LLM Response Information</div>
<div className="smtcmp-llm-info-tokens">
<div className="smtcmp-llm-info-tokens-header">Token Count</div>
<div className="smtcmp-llm-info-tokens-grid">
<div className="smtcmp-llm-info-token-row">
<ArrowUp className="smtcmp-llm-info-icon--input" />
<span>Input:</span>
<span className="smtcmp-llm-info-token-value">
{usage.prompt_tokens}
</span>
</div>
<div className="smtcmp-llm-info-token-row">
<ArrowDown className="smtcmp-llm-info-icon--output" />
<span>Output:</span>
<span className="smtcmp-llm-info-token-value">
{usage.completion_tokens}
</span>
</div>
<div className="smtcmp-llm-info-token-row smtcmp-llm-info-token-total">
<ArrowRightLeft className="smtcmp-llm-info-icon--total" />
<span>Total:</span>
<span className="smtcmp-llm-info-token-value">
{usage.total_tokens}
</span>
</div>
</div>
</div>
<div className="smtcmp-llm-info-footer-row">
<Coins className="smtcmp-llm-info-icon--footer" />
<span>Estimated Price:</span>
<span className="smtcmp-llm-info-footer-value">
{estimatedPrice === null
? 'Not available'
: `$${estimatedPrice.toFixed(4)}`}
</span>
</div>
<div className="smtcmp-llm-info-footer-row">
<Cpu className="smtcmp-llm-info-icon--footer" />
<span>Model:</span>
<span className="smtcmp-llm-info-footer-value smtcmp-llm-info-model">
{model ?? 'Not available'}
</span>
</div>
</Popover.Content>
) : (
<Popover.Content className="smtcmp-popover-content">
<div>Usage statistics are not available for this model</div>
</Popover.Content>
)}
</Popover.Root>
)
}
21 changes: 21 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,25 @@ export const EMBEDDING_MODEL_OPTIONS: EmbeddingModelOption[] = [
},
]

// Pricing in dollars per million tokens
type ModelPricing = {
input: number
output: number
}

export const OPENAI_PRICES: Record<string, ModelPricing> = {
'gpt-4o': { input: 2.5, output: 10 },
'gpt-4o-mini': { input: 0.15, output: 0.6 },
}

export const ANTHROPIC_PRICES: Record<string, ModelPricing> = {
'claude-3-5-sonnet-latest': { input: 3, output: 15 },
'claude-3-5-haiku-latest': { input: 1, output: 5 },
}

export const GROQ_PRICES: Record<string, ModelPricing> = {
'llama-3.1-70b-versatile': { input: 0.59, output: 0.79 },
'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
}

export const PGLITE_DB_PATH = '.smtcmp_vector_db.tar.gz'
Loading

0 comments on commit 72402a9

Please sign in to comment.