Integrate pricing with canonical model (#6130)
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useModelAndProvider } from '../ModelAndProviderContext';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
import { CoinIcon } from '../icons';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
||||
import {
|
||||
getCostForModel,
|
||||
initializeCostDatabase,
|
||||
updateAllModelCosts,
|
||||
fetchAndCachePricing,
|
||||
} from '../../utils/costDatabase';
|
||||
import { fetchModelPricing } from '../../utils/pricing';
|
||||
import { PricingData } from '../../api';
|
||||
|
||||
interface CostTrackerProps {
|
||||
inputTokens?: number;
|
||||
@@ -24,18 +19,10 @@ interface CostTrackerProps {
|
||||
|
||||
export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: CostTrackerProps) {
|
||||
const { currentModel, currentProvider } = useModelAndProvider();
|
||||
const { getProviders } = useConfig();
|
||||
const [costInfo, setCostInfo] = useState<{
|
||||
input_token_cost?: number;
|
||||
output_token_cost?: number;
|
||||
currency?: string;
|
||||
} | null>(null);
|
||||
const [costInfo, setCostInfo] = useState<PricingData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showPricing, setShowPricing] = useState(true);
|
||||
const [pricingFailed, setPricingFailed] = useState(false);
|
||||
const [modelNotFound, setModelNotFound] = useState(false);
|
||||
const [hasAttemptedFetch, setHasAttemptedFetch] = useState(false);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
|
||||
// Check if pricing is enabled
|
||||
useEffect(() => {
|
||||
@@ -44,33 +31,11 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
setShowPricing(stored !== 'false');
|
||||
};
|
||||
|
||||
// Check on mount
|
||||
checkPricingSetting();
|
||||
|
||||
// Listen for storage changes
|
||||
window.addEventListener('storage', checkPricingSetting);
|
||||
return () => window.removeEventListener('storage', checkPricingSetting);
|
||||
}, []);
|
||||
|
||||
// Set initial load complete after a short delay
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setInitialLoadComplete(true);
|
||||
}, 3000); // Give 3 seconds for initial load
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Debug log props removed
|
||||
|
||||
// Initialize cost database on mount
|
||||
useEffect(() => {
|
||||
initializeCostDatabase();
|
||||
|
||||
// Update costs for all models in background
|
||||
updateAllModelCosts().catch(() => {});
|
||||
}, [getProviders]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCostInfo = async () => {
|
||||
if (!currentModel || !currentProvider) {
|
||||
@@ -78,49 +43,20 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// First check sync cache
|
||||
let costData = getCostForModel(currentProvider, currentModel);
|
||||
|
||||
const costData = await fetchModelPricing(currentProvider, currentModel);
|
||||
if (costData) {
|
||||
// We have cached data
|
||||
setCostInfo(costData);
|
||||
setPricingFailed(false);
|
||||
setModelNotFound(false);
|
||||
setIsLoading(false);
|
||||
setHasAttemptedFetch(true);
|
||||
} else {
|
||||
// Need to fetch from backend
|
||||
setIsLoading(true);
|
||||
const result = await fetchAndCachePricing(currentProvider, currentModel);
|
||||
setHasAttemptedFetch(true);
|
||||
|
||||
if (result && result.costInfo) {
|
||||
setCostInfo(result.costInfo);
|
||||
setPricingFailed(false);
|
||||
setModelNotFound(false);
|
||||
} else if (result && result.error === 'model_not_found') {
|
||||
// Model not found in pricing database, but API call succeeded
|
||||
setModelNotFound(true);
|
||||
setPricingFailed(false);
|
||||
} else {
|
||||
// API call failed or other error
|
||||
const freeProviders = ['ollama', 'local', 'localhost'];
|
||||
if (!freeProviders.includes(currentProvider.toLowerCase())) {
|
||||
setPricingFailed(true);
|
||||
setModelNotFound(false);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
setPricingFailed(true);
|
||||
setCostInfo(null);
|
||||
}
|
||||
} catch {
|
||||
setHasAttemptedFetch(true);
|
||||
// Only set pricing failed if we're not dealing with a known free provider
|
||||
const freeProviders = ['ollama', 'local', 'localhost'];
|
||||
if (!freeProviders.includes(currentProvider.toLowerCase())) {
|
||||
setPricingFailed(true);
|
||||
setModelNotFound(false);
|
||||
}
|
||||
setPricingFailed(true);
|
||||
setCostInfo(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
@@ -221,10 +157,9 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
|
||||
// Otherwise show as unavailable
|
||||
const getUnavailableTooltip = () => {
|
||||
if (pricingFailed && hasAttemptedFetch && initialLoadComplete) {
|
||||
return `Pricing data unavailable - OpenRouter connection failed. Click refresh in settings to retry.`;
|
||||
if (pricingFailed) {
|
||||
return `Pricing data unavailable for ${currentModel}`;
|
||||
}
|
||||
// If we reach here, it must be modelNotFound (since we only get here after attempting fetch)
|
||||
return `Cost data not available for ${currentModel} (${inputTokens.toLocaleString()} input, ${outputTokens.toLocaleString()} output tokens)`;
|
||||
};
|
||||
|
||||
@@ -249,12 +184,8 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
// Build tooltip content
|
||||
const getTooltipContent = (): string => {
|
||||
// Handle error states first
|
||||
if (pricingFailed && hasAttemptedFetch && initialLoadComplete) {
|
||||
return `Pricing data unavailable - OpenRouter connection failed. Click refresh in settings to retry.`;
|
||||
}
|
||||
|
||||
if (modelNotFound && hasAttemptedFetch && initialLoadComplete) {
|
||||
return `Pricing not available for ${currentProvider}/${currentModel}. This model may not be supported by the pricing service.`;
|
||||
if (pricingFailed) {
|
||||
return `Pricing data unavailable for ${currentProvider}/${currentModel}`;
|
||||
}
|
||||
|
||||
// Handle session costs
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import UpdateSection from './UpdateSection';
|
||||
import TunnelSection from '../tunnel/TunnelSection';
|
||||
|
||||
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
|
||||
import { getApiUrl } from '../../../config';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
import ThemeSelector from '../../GooseSidebar/ThemeSelector';
|
||||
import BlockLogoBlack from './icons/block-lockup_black.png';
|
||||
@@ -26,9 +25,6 @@ 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 [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const updateSectionRef = useRef<HTMLDivElement>(null);
|
||||
@@ -66,71 +62,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
setShowPricing(stored !== 'false');
|
||||
}, []);
|
||||
|
||||
// Check pricing status on mount
|
||||
useEffect(() => {
|
||||
checkPricingStatus();
|
||||
}, []);
|
||||
|
||||
const checkPricingStatus = async () => {
|
||||
try {
|
||||
const apiUrl = getApiUrl('/config/pricing');
|
||||
const secretKey = await window.electron.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();
|
||||
setPricingStatus('success');
|
||||
setLastFetchTime(new Date());
|
||||
} else {
|
||||
setPricingStatus('error');
|
||||
}
|
||||
} catch {
|
||||
setPricingStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshPricing = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const apiUrl = getApiUrl('/config/pricing');
|
||||
const secretKey = await window.electron.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 {
|
||||
setPricingStatus('error');
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle scrolling to update section
|
||||
useEffect(() => {
|
||||
if (scrollToSection === 'update' && updateSectionRef.current) {
|
||||
@@ -326,69 +257,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pricing Status - only show if cost tracking is enabled */}
|
||||
{COST_TRACKING_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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user