[Feat] UI - Allow setting Provider Discounts on UI (#15550)

* fix cost_discount_config

* add CostBreakdown

* fix: set_cost_breakdown

* test_cost_discount_vertex_ai

* docs fix

* docs fix discounts

* docs fix

* docs custom pricing

* docs fix

* fixes for getting cost breakdown in response headers

* test - response headers wth discount

* - feat: allow setting cost tracking settings

* add cost tracking endpoints

* feat add cost_tracking_settings_router

* add validation when setting cost settings

* add CostTrackingSettings

* add page

* refactor

* use simple table

* add ProviderDiscountTable

* fix ui

* save

* fix: discount settings

* fix edits

* add settings

* add HelpLink

* add help link

* docs HelpLink

* docs fix

* fix how it works

* fix layout

* test TestCostTrackingSettings

* fix Dict
This commit is contained in:
Ishaan Jaff 2025-10-14 20:08:18 -07:00 • committed by GitHub
parent a6c57cb5bd
commit 5b7e2fd778
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1831 additions and 2 deletions

View file

@ -0,0 +1,165 @@
"""
COST TRACKING SETTINGS MANAGEMENT
Endpoints for managing cost discount configuration
GET /config/cost_discount_config - Get current cost discount configuration
PATCH /config/cost_discount_config - Update cost discount configuration
"""
from typing import Dict
from fastapi import APIRouter, Depends, HTTPException
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.utils import LlmProvidersSet
router = APIRouter()
@router.get(
"/config/cost_discount_config",
tags=["Cost Tracking"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_cost_discount_config(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Get current cost discount configuration.
Returns the cost_discount_config from litellm_settings.
"""
from litellm.proxy.proxy_server import prisma_client, proxy_config
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
try:
# Load config from DB
config = await proxy_config.get_config()
# Get cost_discount_config from litellm_settings
litellm_settings = config.get("litellm_settings", {})
cost_discount_config = litellm_settings.get("cost_discount_config", {})
return {"values": cost_discount_config}
except Exception as e:
verbose_proxy_logger.error(
f"Error fetching cost discount config: {str(e)}"
)
return {"values": {}}
@router.patch(
"/config/cost_discount_config",
tags=["Cost Tracking"],
dependencies=[Depends(user_api_key_auth)],
)
async def update_cost_discount_config(
cost_discount_config: Dict[str, float],
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Update cost discount configuration.
Updates the cost_discount_config in litellm_settings.
Discounts should be between 0 and 1 (e.g., 0.05 = 5% discount).
Example:
```json
{
"vertex_ai": 0.05,
"gemini": 0.05,
"openai": 0.01
}
```
"""
from litellm.proxy.proxy_server import (
prisma_client,
proxy_config,
store_model_in_db,
)
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
if store_model_in_db is not True:
raise HTTPException(
status_code=500,
detail={
"error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature."
},
)
# Validate that all providers are valid LiteLLM providers
invalid_providers = []
for provider in cost_discount_config.keys():
if provider not in LlmProvidersSet:
invalid_providers.append(provider)
if invalid_providers:
raise HTTPException(
status_code=400,
detail={
"error": f"Invalid provider(s): {', '.join(invalid_providers)}. Must be valid LiteLLM providers. See https://docs.litellm.ai/docs/providers for the full list."
},
)
# Validate discount values are between 0 and 1
for provider, discount in cost_discount_config.items():
if not isinstance(discount, (int, float)):
raise HTTPException(
status_code=400,
detail=f"Discount for {provider} must be a number"
)
if not (0 <= discount <= 1):
raise HTTPException(
status_code=400,
detail=f"Discount for {provider} must be between 0 and 1 (0% to 100%)"
)
try:
# Load existing config
config = await proxy_config.get_config()
# Ensure litellm_settings exists
if "litellm_settings" not in config:
config["litellm_settings"] = {}
# Update cost_discount_config
config["litellm_settings"]["cost_discount_config"] = cost_discount_config
# Save the updated config to DB
await proxy_config.save_config(new_config=config)
# Update in-memory litellm.cost_discount_config
litellm.cost_discount_config = cost_discount_config
verbose_proxy_logger.info(
f"Updated cost_discount_config: {cost_discount_config}"
)
return {
"message": "Cost discount configuration updated successfully",
"status": "success",
"values": cost_discount_config
}
except Exception as e:
verbose_proxy_logger.error(
f"Error updating cost discount config: {str(e)}"
)
raise HTTPException(
status_code=500,
detail={"error": f"Failed to update cost discount config: {str(e)}"}
)

View file

@ -253,13 +253,18 @@ from litellm.proxy.management_endpoints.callback_management_endpoints import (
router as callback_management_endpoints_router,
)
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy.management_endpoints.cost_tracking_settings import (
router as cost_tracking_settings_router,
)
from litellm.proxy.management_endpoints.customer_endpoints import (
router as customer_router,
)
from litellm.proxy.management_endpoints.internal_user_endpoints import (
router as internal_user_router,
)
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
from litellm.proxy.management_endpoints.internal_user_endpoints import (
user_update,
)
from litellm.proxy.management_endpoints.key_management_endpoints import (
delete_verification_tokens,
duration_in_seconds,
@ -306,7 +311,9 @@ from litellm.proxy.middleware.prometheus_auth_middleware import PrometheusAuthMi
from litellm.proxy.openai_files_endpoints.files_endpoints import (
router as openai_files_router,
)
from litellm.proxy.openai_files_endpoints.files_endpoints import set_files_config
from litellm.proxy.openai_files_endpoints.files_endpoints import (
set_files_config,
)
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
passthrough_endpoint_router,
)
@ -9799,6 +9806,7 @@ app.include_router(team_callback_router)
app.include_router(budget_management_router)
app.include_router(model_management_router)
app.include_router(tag_management_router)
app.include_router(cost_tracking_settings_router)
app.include_router(user_agent_analytics_router)
app.include_router(enterprise_router)
app.include_router(ui_discovery_endpoints_router)

View file

@ -0,0 +1,272 @@
"""
Tests for cost tracking settings management endpoints.
Tests the GET and PATCH endpoints for managing cost discount configuration.
"""
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi.testclient import TestClient
sys.path.insert(
0, os.path.abspath("../../../..")
)
import litellm
from litellm.proxy.management_endpoints.cost_tracking_settings import router
from litellm.proxy.proxy_server import app
client = TestClient(app)
class TestCostTrackingSettings:
"""Test suite for cost tracking settings endpoints"""
@pytest.mark.asyncio
async def test_get_cost_discount_config_success(self):
"""
Test GET /config/cost_discount_config endpoint successfully retrieves configuration.
"""
# Mock the proxy_config to return a config with cost_discount_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(
return_value={
"litellm_settings": {
"cost_discount_config": {
"vertex_ai": 0.05,
"gemini": 0.05,
"openai": 0.01,
}
}
}
)
mock_prisma_client = MagicMock()
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
):
# Make request
response = client.get(
"/config/cost_discount_config",
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response
assert response.status_code == 200
response_data = response.json()
assert "values" in response_data
assert response_data["values"]["vertex_ai"] == 0.05
assert response_data["values"]["gemini"] == 0.05
assert response_data["values"]["openai"] == 0.01
# Verify get_config was called
mock_proxy_config.get_config.assert_called_once()
@pytest.mark.asyncio
async def test_get_cost_discount_config_empty(self):
"""
Test GET /config/cost_discount_config endpoint returns empty config when not set.
"""
# Mock the proxy_config to return a config without cost_discount_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(
return_value={"litellm_settings": {}}
)
mock_prisma_client = MagicMock()
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
):
# Make request
response = client.get(
"/config/cost_discount_config",
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response
assert response.status_code == 200
response_data = response.json()
assert "values" in response_data
assert response_data["values"] == {}
@pytest.mark.asyncio
async def test_update_cost_discount_config_success(self):
"""
Test PATCH /config/cost_discount_config endpoint successfully updates configuration.
"""
# Mock the proxy_config
mock_proxy_config = AsyncMock()
mock_proxy_config.get_config = AsyncMock(
return_value={"litellm_settings": {}}
)
mock_proxy_config.save_config = AsyncMock()
mock_prisma_client = MagicMock()
mock_store_model_in_db = True
# Test data
test_discount_config = {
"vertex_ai": 0.05,
"gemini": 0.05,
"openai": 0.01,
}
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
), patch(
"litellm.proxy.proxy_server.store_model_in_db",
mock_store_model_in_db,
), patch.object(litellm, "cost_discount_config", {}):
# Make request
response = client.patch(
"/config/cost_discount_config",
json=test_discount_config,
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response
assert response.status_code == 200
response_data = response.json()
assert response_data["status"] == "success"
assert "message" in response_data
assert "values" in response_data
assert response_data["values"]["vertex_ai"] == 0.05
assert response_data["values"]["gemini"] == 0.05
assert response_data["values"]["openai"] == 0.01
# Verify config was saved
mock_proxy_config.save_config.assert_called_once()
# Verify litellm.cost_discount_config was updated
assert litellm.cost_discount_config == test_discount_config
@pytest.mark.asyncio
async def test_update_cost_discount_config_invalid_provider(self):
"""
Test PATCH /config/cost_discount_config endpoint rejects invalid provider names.
"""
mock_proxy_config = AsyncMock()
mock_prisma_client = MagicMock()
mock_store_model_in_db = True
# Test data with invalid provider
test_discount_config = {
"invalid_provider": 0.05,
"openai": 0.01,
}
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
), patch(
"litellm.proxy.proxy_server.store_model_in_db",
mock_store_model_in_db,
):
# Make request
response = client.patch(
"/config/cost_discount_config",
json=test_discount_config,
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response - should fail with 400
assert response.status_code == 400
response_data = response.json()
assert "error" in response_data["detail"]
assert "invalid_provider" in response_data["detail"]["error"]
@pytest.mark.asyncio
async def test_update_cost_discount_config_invalid_discount_value(self):
"""
Test PATCH /config/cost_discount_config endpoint rejects discount values outside 0-1 range.
"""
mock_proxy_config = AsyncMock()
mock_prisma_client = MagicMock()
mock_store_model_in_db = True
# Test data with invalid discount value (> 1)
test_discount_config = {
"openai": 1.5, # Invalid: greater than 1
}
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
), patch(
"litellm.proxy.proxy_server.store_model_in_db",
mock_store_model_in_db,
):
# Make request
response = client.patch(
"/config/cost_discount_config",
json=test_discount_config,
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response - should fail with 400
assert response.status_code == 400
response_data = response.json()
assert "detail" in response_data
assert "between 0 and 1" in response_data["detail"]
@pytest.mark.asyncio
async def test_update_cost_discount_config_no_store_model_in_db(self):
"""
Test PATCH /config/cost_discount_config endpoint fails when STORE_MODEL_IN_DB is not enabled.
"""
mock_proxy_config = AsyncMock()
mock_prisma_client = MagicMock()
mock_store_model_in_db = False # Not enabled
test_discount_config = {
"openai": 0.05,
}
with patch(
"litellm.proxy.proxy_server.prisma_client",
mock_prisma_client,
), patch(
"litellm.proxy.proxy_server.proxy_config",
mock_proxy_config,
), patch(
"litellm.proxy.proxy_server.store_model_in_db",
mock_store_model_in_db,
):
# Make request
response = client.patch(
"/config/cost_discount_config",
json=test_discount_config,
headers={"Authorization": "Bearer sk-1234"},
)
# Verify response - should fail with 500
assert response.status_code == 500
response_data = response.json()
assert "error" in response_data["detail"]
assert "STORE_MODEL_IN_DB" in response_data["detail"]["error"]

View file

@ -35,6 +35,7 @@ import { MCPServers } from "@/components/mcp_tools";
import TagManagement from "@/components/tag_management";
import VectorStoreManagement from "@/components/vector_store_management";
import UIThemeSettings from "@/components/ui_theme_settings";
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { cx } from "@/lib/cva.config";
import useFeatureFlags from "@/hooks/useFeatureFlags";
@ -426,6 +427,8 @@ export default function CreateKeyPage() {
/>
) : page == "ui-theme" ? (
<UIThemeSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "cost-tracking-settings" ? (
<CostTrackingSettings userID={userID} userRole={userRole} accessToken={accessToken} />
) : page == "model-hub-table" ? (
<ModelHubTable
accessToken={accessToken}

View file

@ -0,0 +1,110 @@
import React from "react";
import { Text, TextInput, Button } from "@tremor/react";
import { Select as AntdSelect, Form, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Providers, provider_map, providerLogoMap } from "../provider_info_helpers";
import { DiscountConfig } from "./types";
import { handleImageError } from "./provider_display_helpers";
interface AddProviderFormProps {
discountConfig: DiscountConfig;
selectedProvider: string | undefined;
newDiscount: string;
onProviderChange: (provider: string | undefined) => void;
onDiscountChange: (discount: string) => void;
onAddProvider: () => void;
}
const AddProviderForm: React.FC<AddProviderFormProps> = ({
discountConfig,
selectedProvider,
newDiscount,
onProviderChange,
onDiscountChange,
onAddProvider,
}) => {
return (
<div className="space-y-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Provider
<Tooltip title="Select the LLM provider you want to configure a discount for">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
rules={[{ required: true, message: "Please select a provider" }]}
>
<AntdSelect
showSearch
placeholder="Select provider"
value={selectedProvider}
onChange={onProviderChange}
style={{ width: "100%" }}
size="large"
optionFilterProp="children"
filterOption={(input, option) =>
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => {
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
// Only show providers that don't already have a discount configured
if (providerValue && discountConfig[providerValue]) {
return null;
}
return (
<AntdSelect.Option key={providerEnum} value={providerEnum} label={providerDisplayName}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
);
})}
</AntdSelect>
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Discount Percentage
<Tooltip title="Enter a percentage value (e.g., 5 for 5% discount)">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
rules={[{ required: true, message: "Please enter a discount percentage" }]}
>
<div className="flex items-center gap-2">
<TextInput
placeholder="5"
value={newDiscount}
onValueChange={onDiscountChange}
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 flex-1"
/>
<span className="text-gray-600">%</span>
</div>
</Form.Item>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
<Button
variant="primary"
onClick={onAddProvider}
disabled={!selectedProvider || !newDiscount}
>
Add Provider Discount
</Button>
</div>
</div>
);
};
export default AddProviderForm;

View file

@ -0,0 +1,301 @@
import React, { useState, useEffect } from "react";
import { Card, Title, Text, Subtitle, Grid, Col, Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Modal, Form } from "antd";
import { getProxyBaseUrl } from "@/components/networking";
import NotificationsManager from "../molecules/notifications_manager";
import { Providers, provider_map } from "../provider_info_helpers";
import { CostTrackingSettingsProps, DiscountConfig } from "./types";
import { getProviderBackendValue } from "./provider_display_helpers";
import ProviderDiscountTable from "./provider_discount_table";
import AddProviderForm from "./add_provider_form";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { DocsMenu } from "../HelpLink";
import HowItWorks from "./how_it_works";
const DOCS_LINKS = [
{ label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" },
{ label: "Spend tracking", href: "https://docs.litellm.ai/docs/proxy/cost_tracking" },
];
const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
userID,
userRole,
accessToken
}) => {
const [discountConfig, setDiscountConfig] = useState<DiscountConfig>({});
const [selectedProvider, setSelectedProvider] = useState<string | undefined>(undefined);
const [newDiscount, setNewDiscount] = useState<string>("");
const [isFetching, setIsFetching] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [form] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
useEffect(() => {
if (accessToken) {
fetchDiscountConfig();
}
}, [accessToken]);
const fetchDiscountConfig = async () => {
setIsFetching(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
setDiscountConfig(data.values || {});
} else {
console.error("Failed to fetch discount config");
}
} catch (error) {
console.error("Error fetching discount config:", error);
NotificationsManager.fromBackend("Failed to fetch discount configuration");
} finally {
setIsFetching(false);
}
};
const saveDiscountConfig = async (config: DiscountConfig) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
if (response.ok) {
NotificationsManager.success("Discount configuration updated successfully");
await fetchDiscountConfig();
} else {
const errorData = await response.json();
const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
NotificationsManager.fromBackend(errorMessage);
}
} catch (error) {
console.error("Error updating discount config:", error);
NotificationsManager.fromBackend("Failed to update discount configuration");
}
};
const handleAddProvider = async () => {
if (!selectedProvider || !newDiscount) {
NotificationsManager.fromBackend("Please select a provider and enter discount percentage");
return;
}
const percentageValue = parseFloat(newDiscount);
if (isNaN(percentageValue) || percentageValue < 0 || percentageValue > 100) {
NotificationsManager.fromBackend("Discount must be between 0% and 100%");
return;
}
const providerValue = getProviderBackendValue(selectedProvider);
if (!providerValue) {
NotificationsManager.fromBackend("Invalid provider selected");
return;
}
if (discountConfig[providerValue]) {
NotificationsManager.fromBackend(
`Discount for ${Providers[selectedProvider as keyof typeof Providers]} already exists. Edit it in the table above.`
);
return;
}
// Convert percentage to decimal for storage
const discountValue = percentageValue / 100;
const updatedConfig = {
...discountConfig,
[providerValue]: discountValue,
};
setDiscountConfig(updatedConfig);
await saveDiscountConfig(updatedConfig);
setSelectedProvider(undefined);
setNewDiscount("");
setIsModalVisible(false);
};
const handleModalCancel = () => {
setIsModalVisible(false);
form.resetFields();
setSelectedProvider(undefined);
setNewDiscount("");
};
const handleFormSubmit = (values: any) => {
handleAddProvider();
};
const handleRemoveProvider = async (provider: string, providerDisplayName: string) => {
modal.confirm({
title: 'Remove Provider Discount',
icon: <ExclamationCircleOutlined />,
content: `Are you sure you want to remove the discount for ${providerDisplayName}?`,
okText: 'Remove',
okType: 'danger',
cancelText: 'Cancel',
onOk: async () => {
const updatedConfig = { ...discountConfig };
delete updatedConfig[provider];
setDiscountConfig(updatedConfig);
await saveDiscountConfig(updatedConfig);
},
});
};
const handleDiscountChange = async (provider: string, value: string) => {
const discountValue = parseFloat(value);
if (!isNaN(discountValue) && discountValue >= 0 && discountValue <= 1) {
const updatedConfig = {
...discountConfig,
[provider]: discountValue,
};
setDiscountConfig(updatedConfig);
await saveDiscountConfig(updatedConfig);
}
};
if (!accessToken) {
return null;
}
return (
<div className="w-full p-8">
{contextHolder}
{/* Header Section - Outside the card */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-6">
<div>
<div className="flex items-center gap-2">
<Title>Cost Tracking Settings</Title>
<DocsMenu items={DOCS_LINKS} />
</div>
<Text className="text-gray-500 mt-1">
Configure cost discounts for different LLM providers. Changes are saved automatically.
</Text>
</div>
<Button
onClick={() => setIsModalVisible(true)}
className="mt-4 md:mt-0"
>
+ Add Provider Discount
</Button>
</div>
{/* Main Content Card with Tabs */}
<div className="bg-white rounded-lg shadow w-full max-w-full">
<TabGroup>
<TabList className="px-6 pt-4">
<Tab>Provider Discounts</Tab>
<Tab>Test It</Tab>
</TabList>
<TabPanels>
<TabPanel>
{isFetching ? (
<div className="py-12 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</div>
) : Object.keys(discountConfig).length > 0 ? (
<div className="p-6">
<ProviderDiscountTable
discountConfig={discountConfig}
onDiscountChange={handleDiscountChange}
onRemoveProvider={handleRemoveProvider}
/>
</div>
) : (
<div className="py-16 px-6 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<Text className="text-gray-700 font-medium mb-2">
No provider discounts configured
</Text>
<Text className="text-gray-500 text-sm">
Click "Add Provider Discount" to get started
</Text>
</div>
)}
</TabPanel>
<TabPanel>
<div className="px-6 pb-4">
<HowItWorks />
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<h2 className="text-xl font-semibold text-gray-900">Add Provider Discount</h2>
</div>
}
open={isModalVisible}
width={1000}
onCancel={handleModalCancel}
footer={null}
className="top-8"
styles={{
body: { padding: "24px" },
header: { padding: "24px 24px 0 24px", border: "none" },
}}
>
<div className="mt-6">
<Text className="text-sm text-gray-600 mb-6">
Select a provider and set its discount percentage. Enter a value between 0% and 100% (e.g., 5 for a 5% discount).
</Text>
<Form
form={form}
onFinish={handleFormSubmit}
layout="vertical"
className="space-y-6"
>
<AddProviderForm
discountConfig={discountConfig}
selectedProvider={selectedProvider}
newDiscount={newDiscount}
onProviderChange={setSelectedProvider}
onDiscountChange={setNewDiscount}
onAddProvider={handleAddProvider}
/>
</Form>
</div>
</Modal>
</div>
);
};
export default CostTrackingSettings;

View file

@ -0,0 +1,148 @@
import React, { useState, useMemo } from "react";
import { Title, Text, TextInput } from "@tremor/react";
import CodeBlock from "@/app/(dashboard)/api-reference/components/CodeBlock";
const HowItWorks: React.FC = () => {
const [responseCost, setResponseCost] = useState("");
const [discountAmount, setDiscountAmount] = useState("");
const calculatedDiscount = useMemo(() => {
const cost = parseFloat(responseCost);
const discount = parseFloat(discountAmount);
if (isNaN(cost) || isNaN(discount) || cost === 0 || discount === 0) {
return null;
}
const originalCost = cost + discount;
const discountPercentage = (discount / originalCost) * 100;
return {
originalCost: originalCost.toFixed(10),
finalCost: cost.toFixed(10),
discountAmount: discount.toFixed(10),
discountPercentage: discountPercentage.toFixed(2),
};
}, [responseCost, discountAmount]);
return (
<div className="space-y-4 pt-2">
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Cost Calculation</Text>
<Text className="text-xs text-gray-600">
Discounts are applied to provider costs: <code className="bg-gray-100 px-1.5 py-0.5 rounded text-xs">final_cost = base_cost × (1 - discount%/100)</code>
</Text>
</div>
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Example</Text>
<Text className="text-xs text-gray-600">
A 5% discount on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50
</Text>
</div>
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Valid Range</Text>
<Text className="text-xs text-gray-600">
Discount percentages must be between 0% and 100%
</Text>
</div>
<div className="pt-4 border-t border-gray-200">
<Text className="font-medium text-gray-900 text-sm mb-2">Validating Discounts</Text>
<Text className="text-xs text-gray-600 mb-3">
Make a test request and check the response headers to verify discounts are applied:
</Text>
<CodeBlock
language="bash"
code={`curl -X POST -i http://your-proxy:4000/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer sk-1234" \\
-d '{
"model": "gemini/gemini-2.5-pro",
"messages": [{"role": "user", "content": "Hello"}]
}'`}
/>
<Text className="text-xs text-gray-600 mt-3 mb-2">
Look for these headers in the response:
</Text>
<div className="space-y-1.5">
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded text-xs font-mono text-gray-800 whitespace-nowrap">
x-litellm-response-cost
</code>
<Text className="text-xs text-gray-600">Final cost after discount</Text>
</div>
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded text-xs font-mono text-gray-800 whitespace-nowrap">
x-litellm-response-cost-original
</code>
<Text className="text-xs text-gray-600">Original cost before discount</Text>
</div>
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded text-xs font-mono text-gray-800 whitespace-nowrap">
x-litellm-response-cost-discount-amount
</code>
<Text className="text-xs text-gray-600">Amount discounted</Text>
</div>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<Text className="font-medium text-gray-900 text-sm mb-3">Discount Calculator</Text>
<Text className="text-xs text-gray-600 mb-3">
Enter values from your response headers to verify the discount:
</Text>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Response Cost (x-litellm-response-cost)
</label>
<TextInput
placeholder="0.0171938125"
value={responseCost}
onValueChange={setResponseCost}
className="text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Discount Amount (x-litellm-response-cost-discount-amount)
</label>
<TextInput
placeholder="0.0009049375"
value={discountAmount}
onValueChange={setDiscountAmount}
className="text-sm"
/>
</div>
</div>
{calculatedDiscount && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<Text className="text-sm font-medium text-blue-900 mb-2">Calculated Results</Text>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Original Cost:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.originalCost}</code>
</div>
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Final Cost:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.finalCost}</code>
</div>
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Discount Amount:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.discountAmount}</code>
</div>
<div className="flex items-center justify-between pt-2 border-t border-blue-300">
<Text className="text-xs font-semibold text-blue-900">Discount Applied:</Text>
<Text className="text-sm font-bold text-blue-900">{calculatedDiscount.discountPercentage}%</Text>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default HowItWorks;

View file

@ -0,0 +1,8 @@
export { default as CostTrackingSettings } from "./cost_tracking_settings";
export { default as ProviderDiscountTable } from "./provider_discount_table";
export { default as AddProviderForm } from "./add_provider_form";
export { default as HowItWorks } from "./how_it_works";
export type { CostTrackingSettingsProps, DiscountConfig, CostDiscountResponse } from "./types";
export type { ProviderDisplayInfo } from "./provider_display_helpers";
export * from "./provider_display_helpers";

View file

@ -0,0 +1,152 @@
import React, { useState } from "react";
import { TextInput, Icon, Text } from "@tremor/react";
import { TrashIcon, PencilAltIcon, CheckIcon, XIcon } from "@heroicons/react/outline";
import { SimpleTable } from "../common_components/simple_table";
import { DiscountConfig } from "./types";
import { getProviderDisplayInfo, handleImageError } from "./provider_display_helpers";
interface ProviderDiscountTableProps {
discountConfig: DiscountConfig;
onDiscountChange: (provider: string, value: string) => void;
onRemoveProvider: (provider: string, providerDisplayName: string) => void;
}
interface ProviderDiscountRow {
provider: string;
discount: number;
}
const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
discountConfig,
onDiscountChange,
onRemoveProvider,
}) => {
const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [editValue, setEditValue] = useState<string>("");
const handleStartEdit = (provider: string, currentDiscount: number) => {
setEditingProvider(provider);
setEditValue((currentDiscount * 100).toString());
};
const handleSaveEdit = (provider: string) => {
const percentValue = parseFloat(editValue);
if (!isNaN(percentValue) && percentValue >= 0 && percentValue <= 100) {
onDiscountChange(provider, (percentValue / 100).toString());
}
setEditingProvider(null);
setEditValue("");
};
const handleCancelEdit = () => {
setEditingProvider(null);
setEditValue("");
};
const handleKeyDown = (e: React.KeyboardEvent, provider: string) => {
if (e.key === 'Enter') {
handleSaveEdit(provider);
} else if (e.key === 'Escape') {
handleCancelEdit();
}
};
// Convert discount config to array and sort
const data: ProviderDiscountRow[] = Object.entries(discountConfig)
.map(([provider, discount]) => ({ provider, discount }))
.sort((a, b) => {
const displayA = getProviderDisplayInfo(a.provider).displayName;
const displayB = getProviderDisplayInfo(b.provider).displayName;
return displayA.localeCompare(displayB);
});
return (
<SimpleTable
data={data}
columns={[
{
header: "Provider",
cell: (row) => {
const { displayName, logo } = getProviderDisplayInfo(row.provider);
return (
<div className="flex items-center space-x-2">
{logo && (
<img
src={logo}
alt={`${displayName} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, displayName)}
/>
)}
<span className="font-medium">{displayName}</span>
</div>
);
},
},
{
header: "Discount Percentage",
cell: (row) => (
<div className="flex items-center gap-2">
{editingProvider === row.provider ? (
<>
<TextInput
value={editValue}
onValueChange={setEditValue}
onKeyDown={(e) => handleKeyDown(e, row.provider)}
placeholder="5"
className="w-20"
autoFocus
/>
<span className="text-gray-600">%</span>
<Icon
icon={CheckIcon}
size="sm"
onClick={() => handleSaveEdit(row.provider)}
className="cursor-pointer text-green-600 hover:text-green-700"
/>
<Icon
icon={XIcon}
size="sm"
onClick={handleCancelEdit}
className="cursor-pointer text-gray-600 hover:text-gray-700"
/>
</>
) : (
<>
<Text className="font-medium">{(row.discount * 100).toFixed(1)}%</Text>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => handleStartEdit(row.provider, row.discount)}
className="cursor-pointer text-blue-600 hover:text-blue-700"
/>
</>
)}
</div>
),
width: "250px",
},
{
header: "Actions",
cell: (row) => {
const { displayName } = getProviderDisplayInfo(row.provider);
return (
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onRemoveProvider(row.provider, displayName)}
className="cursor-pointer hover:text-red-600"
/>
);
},
width: "80px",
},
]}
getRowKey={(row) => row.provider}
emptyMessage="No provider discounts configured"
/>
);
};
export default ProviderDiscountTable;

View file

@ -0,0 +1,46 @@
import { Providers, provider_map, providerLogoMap } from "../provider_info_helpers";
export interface ProviderDisplayInfo {
displayName: string;
logo: string;
enumKey: string | null;
}
/**
* Convert backend provider value (e.g., "openai") to display info
*/
export const getProviderDisplayInfo = (providerValue: string): ProviderDisplayInfo => {
const enumKey = Object.keys(provider_map).find(
(key) => provider_map[key as keyof typeof provider_map] === providerValue
);
if (enumKey) {
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = providerLogoMap[displayName];
return { displayName, logo, enumKey };
}
return { displayName: providerValue, logo: "", enumKey: null };
};
/**
* Convert provider enum key (e.g., "OpenAI") to backend value (e.g., "openai")
*/
export const getProviderBackendValue = (providerEnum: string): string | null => {
return provider_map[providerEnum as keyof typeof provider_map] || null;
};
/**
* Handle image error by replacing with fallback div
*/
export const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>, fallbackText: string) => {
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement("div");
fallbackDiv.className = "w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = fallbackText.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
};

View file

@ -0,0 +1,14 @@
export interface CostTrackingSettingsProps {
userID: string | null;
userRole: string | null;
accessToken: string | null;
}
export interface DiscountConfig {
[provider: string]: number;
}
export interface CostDiscountResponse {
values: DiscountConfig;
}

View file

@ -0,0 +1,212 @@
import React, { useState, useRef, useEffect } from "react";
import { ExternalLink, ChevronDown } from "lucide-react";
interface HelpLinkProps {
href: string;
children?: React.ReactNode;
variant?: "inline" | "subtle" | "button";
className?: string;
}
interface DocMenuItem {
label: string;
href: string;
}
interface DocsMenuProps {
items: DocMenuItem[];
children?: React.ReactNode;
className?: string;
}
/**
* A reusable component for linking to documentation, styled similar to Linear's help links.
*
* @example
* // Inline "Learn more" style
* <HelpLink href="https://docs.litellm.ai/docs/proxy/custom_pricing">
* Learn more about custom pricing
* </HelpLink>
*
* @example
* // Subtle link (just icon + text, minimal styling)
* <HelpLink href="https://docs.litellm.ai/docs/proxy/cost_tracking" variant="subtle">
* View docs
* </HelpLink>
*
* @example
* // Button style (more prominent)
* <HelpLink href="https://docs.litellm.ai/docs/proxy/custom_pricing" variant="button">
* Custom Pricing Documentation
* </HelpLink>
*/
export const HelpLink: React.FC<HelpLinkProps> = ({
href,
children = "Learn more",
variant = "inline",
className = "",
}) => {
const baseClasses = "inline-flex items-center gap-1.5 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 rounded";
const variantClasses = {
inline: "text-blue-600 hover:text-blue-800 text-sm font-medium hover:underline",
subtle: "text-gray-500 hover:text-gray-700 text-xs",
button: "text-blue-600 hover:text-blue-700 border border-gray-200 hover:border-gray-300 px-3 py-1.5 rounded-md bg-white hover:bg-gray-50 text-sm font-medium shadow-sm",
};
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
title="Open documentation in a new tab"
>
<span>{children}</span>
<ExternalLink className="h-3.5 w-3.5 flex-shrink-0" aria-hidden="true" />
<span className="sr-only">(opens in a new tab)</span>
</a>
);
};
/**
* A minimal help icon with tooltip for inline contextual help.
* Similar to Linear's "?" icons that appear next to labels.
*/
interface HelpIconProps {
content: React.ReactNode;
learnMoreHref?: string;
learnMoreText?: string;
}
export const HelpIcon: React.FC<HelpIconProps> = ({
content,
learnMoreHref,
learnMoreText = "Learn more",
}) => {
const [showTooltip, setShowTooltip] = React.useState(false);
return (
<div className="relative inline-block ml-1.5">
<button
type="button"
className="inline-flex items-center justify-center w-4 h-4 text-gray-400 hover:text-gray-600 transition-colors cursor-help focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-full"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onFocus={() => setShowTooltip(true)}
onBlur={() => setShowTooltip(false)}
aria-label="Help information"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" strokeWidth="1.5" />
<path strokeLinecap="round" d="M12 17h0M12 13.5a1.5 1.5 0 0 1 1-1.415A1.5 1.5 0 1 0 12 9" strokeWidth="1.5" />
</svg>
</button>
{showTooltip && (
<div
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-50 bg-gray-900 text-white p-3 rounded-lg text-xs shadow-lg w-64"
style={{ pointerEvents: "none" }}
>
<div className="mb-2">{content}</div>
{learnMoreHref && (
<a
href={learnMoreHref}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-blue-300 hover:text-blue-200 font-medium"
style={{ pointerEvents: "auto" }}
>
{learnMoreText}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
)}
<div
className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0"
style={{
borderTop: "6px solid rgb(17 24 39)",
borderLeft: "6px solid transparent",
borderRight: "6px solid transparent",
}}
/>
</div>
)}
</div>
);
};
/**
* A dropdown menu for multiple documentation links.
* Linear-style: Single "Docs" button that expands to show multiple relevant links.
*
* @example
* <DocsMenu items={[
* { label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" },
* { label: "Spend tracking", href: "https://docs.litellm.ai/docs/proxy/cost_tracking" }
* ]}>
* Docs
* </DocsMenu>
*/
export const DocsMenu: React.FC<DocsMenuProps> = ({
items,
children = "Docs",
className = "",
}) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
return (
<div className={`relative inline-block ${className}`} ref={menuRef}>
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center gap-1 text-gray-500 hover:text-gray-700 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 rounded px-2 py-1"
aria-expanded={isOpen}
aria-haspopup="true"
>
<span>{children}</span>
<ChevronDown className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`} aria-hidden="true" />
</button>
{isOpen && (
<div className="absolute right-0 mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
{items.map((item, index) => (
<a
key={index}
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => setIsOpen(false)}
>
<span>{item.label}</span>
<ExternalLink className="h-3.5 w-3.5 text-gray-400 flex-shrink-0 ml-2" aria-hidden="true" />
</a>
))}
</div>
)}
</div>
);
};

View file

@ -0,0 +1,71 @@
import React from "react";
import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Text } from "@tremor/react";
export interface SimpleTableColumn<T> {
header: string;
accessor?: keyof T;
cell?: (row: T) => React.ReactNode;
width?: string;
}
interface SimpleTableProps<T> {
data: T[];
columns: SimpleTableColumn<T>[];
isLoading?: boolean;
loadingMessage?: string;
emptyMessage?: string;
getRowKey?: (row: T, index: number) => string;
}
/**
* Simple table component for forms and settings pages
* For complex tables with sorting/filtering, use DataTable from view_logs
*/
export function SimpleTable<T>({
data,
columns,
isLoading = false,
loadingMessage = "Loading...",
emptyMessage = "No data",
getRowKey,
}: SimpleTableProps<T>) {
return (
<Table>
<TableHead>
<TableRow>
{columns.map((column, index) => (
<TableHeaderCell key={index} style={{ width: column.width }}>
{column.header}
</TableHeaderCell>
))}
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="text-center">
<Text className="text-gray-500">{loadingMessage}</Text>
</TableCell>
</TableRow>
) : data.length > 0 ? (
data.map((row, rowIndex) => (
<TableRow key={getRowKey ? getRowKey(row, rowIndex) : rowIndex}>
{columns.map((column, colIndex) => (
<TableCell key={colIndex}>
{column.cell ? column.cell(row) : String(row[column.accessor as keyof T] ?? "")}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="text-center">
<Text className="text-gray-500">{emptyMessage}</Text>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}

View file

@ -0,0 +1,312 @@
import React, { useState, useEffect } from "react";
import {
Card,
Title,
Text,
TextInput,
Button,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Grid,
Col,
Subtitle,
} from "@tremor/react";
import { getProxyBaseUrl } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface CostTrackingSettingsProps {
userID: string | null;
userRole: string | null;
accessToken: string | null;
}
interface DiscountConfig {
[provider: string]: number;
}
const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
userID,
userRole,
accessToken
}) => {
const [discountConfig, setDiscountConfig] = useState<DiscountConfig>({});
const [newProvider, setNewProvider] = useState<string>("");
const [newDiscount, setNewDiscount] = useState<string>("");
const [loading, setLoading] = useState(false);
const [isFetching, setIsFetching] = useState(true);
useEffect(() => {
if (accessToken) {
fetchDiscountConfig();
}
}, [accessToken]);
const fetchDiscountConfig = async () => {
setIsFetching(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
setDiscountConfig(data.values || {});
} else {
console.error("Failed to fetch discount config");
}
} catch (error) {
console.error("Error fetching discount config:", error);
NotificationsManager.fromBackend("Failed to fetch discount configuration");
} finally {
setIsFetching(false);
}
};
const handleSave = async () => {
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(discountConfig),
});
if (response.ok) {
NotificationsManager.success("Cost discount configuration updated successfully");
await fetchDiscountConfig();
} else {
const errorData = await response.json();
const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
NotificationsManager.fromBackend(errorMessage);
}
} catch (error) {
console.error("Error updating discount config:", error);
NotificationsManager.fromBackend("Failed to update discount configuration");
} finally {
setLoading(false);
}
};
const handleAddProvider = () => {
if (!newProvider || !newDiscount) {
NotificationsManager.fromBackend("Please enter both provider and discount value");
return;
}
const discountValue = parseFloat(newDiscount);
if (isNaN(discountValue) || discountValue < 0 || discountValue > 1) {
NotificationsManager.fromBackend("Discount must be between 0 and 1 (0% to 100%)");
return;
}
setDiscountConfig(prev => ({
...prev,
[newProvider.trim()]: discountValue,
}));
setNewProvider("");
setNewDiscount("");
};
const handleRemoveProvider = (provider: string) => {
setDiscountConfig(prev => {
const updated = { ...prev };
delete updated[provider];
return updated;
});
};
const handleDiscountChange = (provider: string, value: string) => {
const discountValue = parseFloat(value);
if (!isNaN(discountValue) && discountValue >= 0 && discountValue <= 1) {
setDiscountConfig(prev => ({
...prev,
[provider]: discountValue,
}));
}
};
if (!accessToken) {
return null;
}
const hasChanges = Object.keys(discountConfig).length > 0;
return (
<div style={{ width: "100%" }} className="relative">
<div className="mb-6">
<Title>Cost Tracking Settings</Title>
<Subtitle>
Configure cost discounts for different LLM providers. Discounts are applied as multipliers.
</Subtitle>
</div>
<Grid numItems={1} className="gap-6">
<Col>
<Card>
<div className="flex justify-between items-start mb-4">
<div>
<Title>Provider Discounts</Title>
<Text className="mt-1 text-sm text-gray-500">
Set custom discount rates per provider (e.g., 0.05 = 5% discount)
</Text>
</div>
<Button
onClick={handleSave}
loading={loading}
disabled={loading || isFetching}
size="sm"
>
Save Changes
</Button>
</div>
{isFetching ? (
<div className="py-8 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</div>
) : (
<>
{Object.keys(discountConfig).length > 0 ? (
<div className="mt-4">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Discount Value</TableHeaderCell>
<TableHeaderCell>Percentage</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(discountConfig)
.sort(([a], [b]) => a.localeCompare(b))
.map(([provider, discount]) => (
<TableRow key={provider}>
<TableCell className="font-medium">{provider}</TableCell>
<TableCell>
<TextInput
value={discount.toString()}
onValueChange={(value) => handleDiscountChange(provider, value)}
placeholder="0.05"
className="w-32"
/>
</TableCell>
<TableCell>
<span className="text-gray-700 font-medium">
{(discount * 100).toFixed(1)}%
</span>
</TableCell>
<TableCell>
<Button
size="xs"
variant="secondary"
color="red"
onClick={() => handleRemoveProvider(provider)}
>
Remove
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="py-8 text-center border border-dashed border-gray-300 rounded-lg">
<Text className="text-gray-500">
No provider discounts configured. Add your first provider below.
</Text>
</div>
)}
<div className="border-t pt-6 mt-6">
<div className="mb-3">
<Text className="font-medium text-gray-900">Add Provider Discount</Text>
<Text className="text-xs text-gray-500 mt-1">
Common providers: vertex_ai, gemini, openai, anthropic, openrouter, bedrock, azure
</Text>
</div>
<Grid numItems={3} className="gap-3">
<Col numColSpan={1}>
<TextInput
placeholder="Provider name"
value={newProvider}
onValueChange={setNewProvider}
/>
</Col>
<Col numColSpan={1}>
<TextInput
placeholder="Discount (0.05 for 5%)"
value={newDiscount}
onValueChange={setNewDiscount}
/>
</Col>
<Col numColSpan={1}>
<Button
onClick={handleAddProvider}
className="w-full"
disabled={!newProvider || !newDiscount}
>
Add Provider
</Button>
</Col>
</Grid>
</div>
</>
)}
</Card>
</Col>
<Col>
<Card>
<Title>How It Works</Title>
<div className="mt-4 space-y-3">
<div>
<Text className="font-medium text-gray-900">Cost Calculation</Text>
<Text className="text-sm text-gray-600 mt-1">
Discounts are applied to provider costs: <code className="bg-gray-100 px-1 py-0.5 rounded">final_cost = base_cost × (1 - discount)</code>
</Text>
</div>
<div>
<Text className="font-medium text-gray-900">Example</Text>
<Text className="text-sm text-gray-600 mt-1">
A 5% discount (0.05) on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50
</Text>
</div>
<div>
<Text className="font-medium text-gray-900">Valid Range</Text>
<Text className="text-sm text-gray-600 mt-1">
Discount values must be between 0 (0%) and 1 (100%)
</Text>
</div>
</div>
</Card>
</Col>
</Grid>
</div>
);
};
export default CostTrackingSettings;

View file

@ -191,6 +191,13 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
icon: <SettingOutlined style={{ fontSize: "18px" }} />,
roles: all_admin_roles,
},
{
key: "27",
page: "cost-tracking-settings",
label: "Cost Tracking",
icon: <BarChartOutlined style={{ fontSize: "18px" }} />,
roles: all_admin_roles,
},
{
key: "14",
page: "ui-theme",