feat: Add comprehensive cost tracking display for LLM usage (#2992)

Co-authored-by: jack <jack@deck.local>
Co-authored-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
jack
2025-06-26 10:46:14 +01:00
committed by GitHub
parent 9d48ed980f
commit d2ff4f3746
24 changed files with 1985 additions and 47 deletions
@@ -1,10 +1,11 @@
import { useState, useEffect, useRef } from 'react';
import { Switch } from '../../ui/switch';
import { Button } from '../../ui/button';
import { Settings } from 'lucide-react';
import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
import Modal from '../../Modal';
import UpdateSection from './UpdateSection';
import { UPDATES_ENABLED } from '../../../updates';
import { getApiUrl, getSecretKey } from '../../../config';
interface AppSettingsSectionProps {
scrollToSection?: string;
@@ -17,6 +18,10 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
const [isMacOS, setIsMacOS] = useState(false);
const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false);
const [showNotificationModal, setShowNotificationModal] = useState(false);
const [pricingStatus, setPricingStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [lastFetchTime, setLastFetchTime] = useState<Date | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [showPricing, setShowPricing] = useState(true);
const updateSectionRef = useRef<HTMLDivElement>(null);
// Check if running on macOS
@@ -24,6 +29,77 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
setIsMacOS(window.electron.platform === 'darwin');
}, []);
// Load show pricing setting
useEffect(() => {
const stored = localStorage.getItem('show_pricing');
setShowPricing(stored !== 'false');
}, []);
// Check pricing status on mount
useEffect(() => {
checkPricingStatus();
}, []);
const checkPricingStatus = async () => {
try {
const apiUrl = getApiUrl('/config/pricing');
const secretKey = getSecretKey();
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (secretKey) {
headers['X-Secret-Key'] = secretKey;
}
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({ configured_only: true }),
});
if (response.ok) {
await response.json(); // Consume the response
setPricingStatus('success');
setLastFetchTime(new Date());
} else {
setPricingStatus('error');
}
} catch (error) {
setPricingStatus('error');
}
};
const handleRefreshPricing = async () => {
setIsRefreshing(true);
try {
const apiUrl = getApiUrl('/config/pricing');
const secretKey = getSecretKey();
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (secretKey) {
headers['X-Secret-Key'] = secretKey;
}
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({ configured_only: false }),
});
if (response.ok) {
setPricingStatus('success');
setLastFetchTime(new Date());
// Trigger a reload of the cost database
window.dispatchEvent(new CustomEvent('pricing-updated'));
} else {
setPricingStatus('error');
}
} catch (error) {
setPricingStatus('error');
} finally {
setIsRefreshing(false);
}
};
// Handle scrolling to update section
useEffect(() => {
if (scrollToSection === 'update' && updateSectionRef.current) {
@@ -99,6 +175,13 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
}
};
const handleShowPricingToggle = (checked: boolean) => {
setShowPricing(checked);
localStorage.setItem('show_pricing', String(checked));
// Trigger storage event for other components
window.dispatchEvent(new CustomEvent('storage'));
};
return (
<section id="appSettings" className="px-8">
<div className="flex justify-between items-center mb-2">
@@ -173,6 +256,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
</div>
)}
{/* Quit Confirmation */}
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-textStandard">Quit Confirmation</h3>
@@ -188,6 +272,87 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
/>
</div>
</div>
{/* Cost Tracking */}
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-textStandard">Cost Tracking</h3>
<p className="text-xs text-textSubtle max-w-md mt-[2px]">
Show model pricing and usage costs
</p>
</div>
<div className="flex items-center">
<Switch
checked={showPricing}
onCheckedChange={handleShowPricingToggle}
variant="mono"
/>
</div>
</div>
{/* Pricing Status - only show if cost tracking is enabled */}
{showPricing && (
<>
<div className="flex items-center justify-between text-xs mb-2 px-4">
<span className="text-textSubtle">Pricing Source:</span>
<a
href="https://openrouter.ai/docs#models"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
OpenRouter Docs
<ExternalLink size={10} />
</a>
</div>
<div className="flex items-center justify-between text-xs mb-2 px-4">
<span className="text-textSubtle">Status:</span>
<div className="flex items-center gap-2">
<span
className={`font-medium ${
pricingStatus === 'success'
? 'text-green-600 dark:text-green-400'
: pricingStatus === 'error'
? 'text-red-600 dark:text-red-400'
: 'text-textSubtle'
}`}
>
{pricingStatus === 'success'
? '✓ Connected'
: pricingStatus === 'error'
? '✗ Failed'
: '... Checking'}
</span>
<button
className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
onClick={handleRefreshPricing}
disabled={isRefreshing}
title="Refresh pricing data"
type="button"
>
<RefreshCw
size={8}
className={`text-textSubtle hover:text-textStandard ${isRefreshing ? 'animate-spin-fast' : ''}`}
/>
</button>
</div>
</div>
{lastFetchTime && (
<div className="flex items-center justify-between text-xs mb-2 px-4">
<span className="text-textSubtle">Last updated:</span>
<span className="text-textSubtle">{lastFetchTime.toLocaleTimeString()}</span>
</div>
)}
{pricingStatus === 'error' && (
<p className="text-xs text-red-600 dark:text-red-400 px-4">
Unable to fetch pricing data. Costs will not be displayed.
</p>
)}
</>
)}
</div>
{/* Help & Feedback Section */}
@@ -43,7 +43,10 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
}, [read]);
// Determine which model to display - activeModel takes priority when lead/worker is active
const displayModel = (isLeadWorkerActive && currentModelInfo?.model) ? currentModelInfo.model : (currentModel || 'Select Model');
const displayModel =
isLeadWorkerActive && currentModelInfo?.model
? currentModelInfo.model
: currentModel || 'Select Model';
const modelMode = currentModelInfo?.mode;
// Update display provider when current provider changes
@@ -106,9 +109,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
>
{displayModel}
{isLeadWorkerActive && modelMode && (
<span className="ml-1 text-[10px] opacity-60">
({modelMode})
</span>
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
)}
</span>
</TooltipTrigger>
@@ -116,9 +117,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
{displayModel}
{isLeadWorkerActive && modelMode && (
<span className="ml-1 text-[10px] opacity-60">
({modelMode})
</span>
<span className="ml-1 text-[10px] opacity-60">({modelMode})</span>
)}
</TooltipContent>
)}
@@ -164,7 +163,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
{isAddModelModalOpen ? (
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
) : null}
{isLeadWorkerModalOpen ? (
<Modal onClose={() => setIsLeadWorkerModalOpen(false)}>
<LeadWorkerSettings onClose={() => setIsLeadWorkerModalOpen(false)} />
@@ -21,7 +21,9 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
const [failureThreshold, setFailureThreshold] = useState<number>(2);
const [fallbackTurns, setFallbackTurns] = useState<number>(2);
const [isEnabled, setIsEnabled] = useState(false);
const [modelOptions, setModelOptions] = useState<{ value: string; label: string; provider: string }[]>([]);
const [modelOptions, setModelOptions] = useState<
{ value: string; label: string; provider: string }[]
>([]);
const [isLoading, setIsLoading] = useState(true);
// Load current configuration
@@ -51,7 +53,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
if (leadTurnsConfig) setLeadTurns(Number(leadTurnsConfig));
if (failureThresholdConfig) setFailureThreshold(Number(failureThresholdConfig));
if (fallbackTurnsConfig) setFallbackTurns(Number(fallbackTurnsConfig));
// Set worker model to current model or from config
const workerModelConfig = await read('GOOSE_MODEL', false);
if (workerModelConfig) {
@@ -59,7 +61,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
} else if (currentModel) {
setWorkerModel(currentModel as string);
}
const workerProviderConfig = await read('GOOSE_PROVIDER', false);
if (workerProviderConfig) {
setWorkerProvider(workerProviderConfig as string);
@@ -69,7 +71,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
const providers = await getProviders(false);
const activeProviders = providers.filter((p) => p.is_configured);
const options: { value: string; label: string; provider: string }[] = [];
activeProviders.forEach(({ metadata, name }) => {
if (metadata.known_models) {
metadata.known_models.forEach((model) => {
@@ -81,7 +83,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
});
}
});
setModelOptions(options);
} catch (error) {
console.error('Error loading configuration:', error);
@@ -184,9 +186,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
placeholder="Select worker model..."
isDisabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">
Fast model for routine execution tasks
</p>
<p className="text-xs text-textSubtle">Fast model for routine execution tasks</p>
</div>
<div className="space-y-4 pt-4 border-t border-borderSubtle">
@@ -242,9 +242,7 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
className="w-20"
disabled={!isEnabled}
/>
<p className="text-xs text-textSubtle">
Turns to use lead model during fallback
</p>
<p className="text-xs text-textSubtle">Turns to use lead model during fallback</p>
</div>
</div>
</div>
@@ -259,4 +257,4 @@ export function LeadWorkerSettings({ onClose }: LeadWorkerSettingsProps) {
</div>
</div>
);
}
}