mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-13 04:54:24 +01:00
feat: add real-time conversation cost tracking with proper token counting
- Add comprehensive ModelPricing service with 100+ models and historical pricing
- Create real-time ConversationCost component that displays in chat header
- Use actual token counts from model APIs instead of client-side estimation
- Fix BaseClient.js to preserve tokenCount in response messages
- Add tokenCount, usage, and tokens fields to message schema
- Update Header component to include ConversationCost display
- Support OpenAI, Anthropic, Google, and other major model providers
- Include color-coded cost display based on amount
- Add 32 unit tests for pricing calculation logic
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
543b617e1c
commit
3edf6fdf6b
9 changed files with 2041 additions and 1 deletions
63
client/src/components/Chat/ConversationCost.tsx
Normal file
63
client/src/components/Chat/ConversationCost.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
type CostDisplay = {
|
||||
totalCost: string;
|
||||
totalCostRaw: number;
|
||||
primaryModel: string;
|
||||
totalTokens: number;
|
||||
lastUpdated: string | number | Date;
|
||||
conversationId?: string;
|
||||
};
|
||||
|
||||
export default function ConversationCost() {
|
||||
const { t } = useTranslation();
|
||||
const { conversationId } = useParams();
|
||||
|
||||
const { data } = useQuery<CostDisplay | null>({
|
||||
queryKey: ['conversationCost', conversationId],
|
||||
enabled: Boolean(conversationId && conversationId !== Constants.NEW_CONVO),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/convos/${conversationId}/cost`, { credentials: 'include' });
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 5_000,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const colorClass = useMemo(() => {
|
||||
const cost = data?.totalCostRaw ?? 0;
|
||||
if (cost < 0.01) return 'text-green-600 dark:text-green-400';
|
||||
if (cost < 0.1) return 'text-yellow-600 dark:text-yellow-400';
|
||||
if (cost < 1) return 'text-orange-600 dark:text-orange-400';
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
}, [data?.totalCostRaw]);
|
||||
|
||||
if (!conversationId || conversationId === Constants.NEW_CONVO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data || data.totalCostRaw === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-400" title={t('com_ui_conversation_cost')}>
|
||||
<span>💰</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tooltipText = `${t('com_ui_conversation_cost')}: ${data.totalCost} | ${t('com_ui_primary_model')}: ${data.primaryModel} | ${t('com_ui_total_tokens')}: ${data.totalTokens.toLocaleString()} | ${t('com_ui_last_updated')}: ${new Date(data.lastUpdated).toLocaleTimeString()}`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors hover:bg-surface-hover" title={tooltipText}>
|
||||
<span className="text-text-tertiary">💰</span>
|
||||
<span className={`font-medium ${colorClass}`}>{data.totalCost}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { useGetStartupConfig } from '~/data-provider';
|
|||
import ExportAndShareMenu from './ExportAndShareMenu';
|
||||
import BookmarkMenu from './Menus/BookmarkMenu';
|
||||
import { TemporaryChat } from './TemporaryChat';
|
||||
import ConversationCost from './ConversationCost';
|
||||
import AddMultiConvo from './AddMultiConvo';
|
||||
import { useHasAccess } from '~/hooks';
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ export default function Header() {
|
|||
{hasAccessToMultiConvo === true && <AddMultiConvo />}
|
||||
{isSmallScreen && (
|
||||
<>
|
||||
<ConversationCost />
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
/>
|
||||
|
|
@ -72,6 +74,7 @@ export default function Header() {
|
|||
</div>
|
||||
{!isSmallScreen && (
|
||||
<div className="flex items-center gap-2">
|
||||
<ConversationCost />
|
||||
<ExportAndShareMenu
|
||||
isSharedButtonEnabled={startupConfig?.sharedLinksEnabled ?? false}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1242,4 +1242,9 @@
|
|||
"com_ui_yes": "Yes",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "You"
|
||||
,
|
||||
"com_ui_conversation_cost": "Conversation cost",
|
||||
"com_ui_last_updated": "Last updated",
|
||||
"com_ui_primary_model": "Model",
|
||||
"com_ui_total_tokens": "Tokens"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue