Merge pull request #15155 from BerriAI/litellm_dev_10_02_2025_p1

Litellm dev 10 02 2025 p1
This commit is contained in:
Krish Dholakia 2025-10-02 19:04:59 -07:00 committed by GitHub
commit 1d548d7235
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 593 additions and 46 deletions

View file

@ -50,6 +50,7 @@
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.0.4",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.17",
@ -95,6 +96,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@ -284,20 +286,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.0",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.6",
"@babel/parser": "^7.28.0",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.0",
"@babel/types": "^7.28.0",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -332,12 +335,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.0",
"@babel/types": "^7.28.0",
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@ -493,13 +497,14 @@
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.3"
"@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@ -609,23 +614,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.2"
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.0"
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -1428,6 +1435,38 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-pure-annotations": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz",
@ -1839,16 +1878,17 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.0",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.0",
"@babel/types": "^7.28.4",
"debug": "^4.3.1"
},
"engines": {
@ -1856,9 +1896,10 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
@ -4051,6 +4092,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz",
"integrity": "sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==",
"license": "MIT",
"peerDependencies": {
"react": ">= 16"
}
@ -4208,6 +4250,16 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -4764,6 +4816,13 @@
"react": ">=18.2.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.0.tgz",
@ -5475,6 +5534,41 @@
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
@ -6403,6 +6497,27 @@
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
},
"node_modules/@vitejs/plugin-react": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.4",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.38",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@vitest/coverage-v8": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
@ -18876,6 +18991,16 @@
"react": ">=18"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",

View file

@ -53,6 +53,7 @@
"@types/react-dom": "^18",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.0.4",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.17",

View file

@ -24,7 +24,7 @@ import {
import AdvancedDatePicker from "./shared/advanced_date_picker";
import { Select } from 'antd';
import { ActivityMetrics, processActivityData } from './activity_metrics';
import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types';
import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from './usage/types';
import { tagDailyActivityCall, teamDailyActivityCall } from './networking';
import TopKeyView from "./top_key_view";
import { formatNumberWithCommas } from "@/utils/dataUtils";
@ -175,8 +175,25 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
};
const getTopAPIKeys = () => {
console.log('debugTags',{spendData})
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
spendData.results.forEach((day) => {
const {breakdown} = day;
const {entities} = breakdown;
console.log('debugTags',{entities})
const tagDictionary = Object.keys(entities).reduce((acc: { [key: string]: TagUsage[] }, entity) => {
const {api_key_breakdown} = entities[entity];
Object.keys(api_key_breakdown).forEach((key) => {
const tagUsage = {tag:entity,usage:api_key_breakdown[key].metrics.spend};
if (acc[key]) {
acc[key].push(tagUsage);
} else {
acc[key] = [tagUsage];
}
})
return acc;
},{})
console.log('debugTags',{tagDictionary})
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
if (!keySpend[key]) {
keySpend[key] = {
@ -193,9 +210,11 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
},
metadata: {
key_alias: metrics.metadata.key_alias,
team_id: metrics.metadata.team_id || null
team_id: metrics.metadata.team_id || null,
tags: tagDictionary[key] || []
}
};
console.log('debugTags',{keySpend})
}
keySpend[key].metrics.spend += metrics.metrics.spend;
keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
@ -218,6 +237,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
.map(([api_key, metrics]) => ({
api_key,
key_alias: metrics.metadata.key_alias || "-", // Using truncated key as alias
tags: metrics.metadata.tags || "-",
spend: metrics.metrics.spend,
}))
.sort((a, b) => b.spend - a.spend)
@ -623,6 +643,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
userRole={userRole}
teams={null}
premiumUser={premiumUser}
showTags={entityType === "tag"}
/>
</Card>
</Col>

View file

@ -269,6 +269,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, user
metadata: {
key_alias: metrics.metadata.key_alias,
team_id: null,
tags: metrics.metadata.tags || [], // This gets key-level tags
},
}
}
@ -284,10 +285,13 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, user
})
})
console.log('debugTags',{keySpend,userSpendData})
return Object.entries(keySpend)
.map(([api_key, metrics]) => ({
api_key,
key_alias: metrics.metadata.key_alias || "-", // Using truncated key as alias
tags: metrics.metadata.tags || [], // This will show key-level tags
spend: metrics.metrics.spend,
}))
.sort((a, b) => b.spend - a.spend)

View file

@ -7,6 +7,8 @@ import { DataTable } from "./view_logs/table"
import { Tooltip } from "antd"
import { Button } from "@tremor/react"
import { formatNumberWithCommas } from "../utils/dataUtils"
import { TagUsage } from "./usage/types"
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/outline"
interface TopKeyViewProps {
topKeys: any[]
@ -15,13 +17,27 @@ interface TopKeyViewProps {
userRole: string | null
teams: any[] | null
premiumUser: boolean
showTags?: boolean
}
const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, accessToken, userID, userRole, teams, premiumUser }) => {
const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, accessToken, userID, userRole, teams, premiumUser, showTags = false }) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
const [selectedKey, setSelectedKey] = useState<string | null>(null)
const [keyData, setKeyData] = useState<any | undefined>(undefined)
const [viewMode, setViewMode] = useState<"chart" | "table">("table")
const [expandedTags, setExpandedTags] = useState<Set<string>>(new Set())
const toggleTagsExpansion = (apiKey: string) => {
setExpandedTags(prev => {
const newSet = new Set(prev)
if (newSet.has(apiKey)) {
newSet.delete(apiKey)
} else {
newSet.add(apiKey)
}
return newSet
})
}
const handleKeyClick = async (item: any) => {
if (!accessToken) return
@ -64,7 +80,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, accessToken, userID, u
}, [isModalOpen])
// Define columns for the table view
const columns = [
const baseColumns = [
{
header: "Key ID",
accessorKey: "api_key",
@ -88,13 +104,74 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, accessToken, userID, u
accessorKey: "key_alias",
cell: (info: any) => info.getValue() || "-",
},
{
header: "Spend (USD)",
accessorKey: "spend",
cell: (info: any) => `$${formatNumberWithCommas(info.getValue(), 2)}`,
},
]
const tagsColumn = {
header: "Tags",
accessorKey: "tags",
cell: (info: any) => {
const tags = info.getValue() as TagUsage[] | undefined;
const apiKey = info.row.original.api_key;
const isExpanded = expandedTags.has(apiKey);
if (!tags || tags.length === 0) {
return "-";
}
const sortedTags = tags.sort((a, b) => b.usage - a.usage);
const displayTags = isExpanded ? sortedTags : sortedTags.slice(0, 2);
const hasMoreTags = tags.length > 2;
return (
<div className="overflow-hidden">
<div className="flex flex-wrap items-center gap-1">
{displayTags.map((tag, index) => (
<Tooltip
key={index}
title={
<div>
<div><span className="text-gray-300">Tag Name:</span> {tag.tag}</div>
<div><span className="text-gray-300">Spend:</span> {tag.usage > 0 && tag.usage < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(tag.usage, 2)}`}</div>
</div>
}
>
<span className="px-2 py-1 bg-gray-100 rounded-full text-xs">
{tag.tag.slice(0, 7)}...
</span>
</Tooltip>
))}
{hasMoreTags && (
<button
onClick={() => toggleTagsExpansion(apiKey)}
className="ml-1 p-1 hover:bg-gray-200 rounded-full transition-colors"
title={isExpanded ? "Show fewer tags" : "Show all tags"}
>
{isExpanded ? (
<ChevronUpIcon className="h-3 w-3 text-gray-500" />
) : (
<ChevronDownIcon className="h-3 w-3 text-gray-500" />
)}
</button>
)}
</div>
</div>
);
}
}
const spendColumn = {
header: "Spend (USD)",
accessorKey: "spend",
cell: (info: any) => {
const value = info.getValue();
return value > 0 && value < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(value, 2)}`;
},
}
const columns = showTags
? [...baseColumns, tagsColumn, spendColumn]
: [...baseColumns, spendColumn]
const processedTopKeys = topKeys.map((k) => ({
...k,
display_key_alias: k.key_alias && k.key_alias.length > 10 ? `${k.key_alias.slice(0, 10)}...` : k.key_alias || "-",

View file

@ -39,6 +39,7 @@ export interface KeyMetricWithMetadata {
export interface KeyMetadata {
key_alias: string | null
team_id: string | null
tags?: {tag:string,usage:number}[]
}
export interface TopApiKeyData {
@ -87,3 +88,8 @@ export interface EntityMetricWithMetadata {
metrics: SpendMetrics
metadata: EntityMetadata
}
export interface TagUsage {
tag: string
usage: number
}

View file

@ -53,7 +53,7 @@ export function DataTable<TData, TValue>({
return (
<div className="rounded-lg custom-border overflow-x-auto w-full max-w-full box-border">
<Table className="[&_td]:py-0.5 [&_th]:py-1 table-fixed w-full box-border" style={{minWidth: '800px'}}>
<Table className="[&_td]:py-0.5 [&_th]:py-1 table-fixed w-full box-border" style={{minWidth: '400px'}}>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>

View file

@ -0,0 +1,306 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderWithProviders, screen, fireEvent } from './test-utils';
import TopKeyView from '../src/components/top_key_view';
import { TagUsage } from '../src/components/usage/types';
// Mock the networking module
vi.mock('../src/components/networking', () => ({
keyInfoV1Call: vi.fn(),
}));
// Mock the transform function
vi.mock('../src/components/key_team_helpers/transform_key_info', () => ({
transformKeyInfo: vi.fn((data) => data),
}));
describe('TopKeyView', () => {
const mockProps = {
topKeys: [],
accessToken: 'test-token',
userID: 'test-user',
userRole: 'admin',
teams: null,
premiumUser: true,
showTags: false
};
const mockKeysWithTags = [
{
api_key: 'key-1',
key_alias: 'Production Key',
tags: [
{ tag: 'production', usage: 0.005 } as TagUsage, // <$0.01
{ tag: 'high-volume', usage: 125.50 } as TagUsage, // High spend
{ tag: 'api-calls', usage: 0.003 } as TagUsage, // <$0.01
],
spend: 125.50
},
{
api_key: 'key-2',
key_alias: 'Staging Key',
tags: [
{ tag: 'staging', usage: 45.75 } as TagUsage, // Medium spend
{ tag: 'testing', usage: 0.008 } as TagUsage, // <$0.01
{ tag: 'development', usage: 12.25 } as TagUsage, // Low spend
],
spend: 58.00
},
{
api_key: 'key-3',
key_alias: 'Development Key',
tags: [
{ tag: 'dev', usage: 0.002 } as TagUsage, // <$0.01
{ tag: 'experimental', usage: 0.001 } as TagUsage, // <$0.01
],
spend: 0.003
}
];
beforeEach(() => {
vi.clearAllMocks();
});
describe('Tags Column Visibility', () => {
it('should not show tags column when showTags is false', () => {
renderWithProviders(<TopKeyView {...mockProps} showTags={false} />);
expect(screen.queryByText('Tags')).not.toBeInTheDocument();
});
it('should show tags column when showTags is true', () => {
renderWithProviders(<TopKeyView {...mockProps} showTags={true} />);
expect(screen.getByText('Tags')).toBeInTheDocument();
});
});
describe('Tags Display and Sorting', () => {
beforeEach(() => {
renderWithProviders(<TopKeyView {...mockProps} topKeys={mockKeysWithTags} showTags={true} />);
});
it('should display tags for each key', () => {
// Check that tags are displayed (truncated to 7 chars + ...)
expect(screen.getByText('product...')).toBeInTheDocument();
expect(screen.getByText('high-vo...')).toBeInTheDocument();
expect(screen.getByText('staging...')).toBeInTheDocument();
});
it('should display top 2 tags by default (sorted by spend)', () => {
// Only the top 2 tags by spend should be visible initially
// Production Key: high-volume (125.50), production (0.005) - sorted by spend
expect(screen.getByText('high-vo...')).toBeInTheDocument();
expect(screen.getByText('product...')).toBeInTheDocument();
// Staging Key: staging (45.75), development (12.25) - sorted by spend
expect(screen.getByText('staging...')).toBeInTheDocument();
expect(screen.getByText('develop...')).toBeInTheDocument();
// Development Key: dev (0.002), experimental (0.001) - sorted by spend
expect(screen.getByText('dev...')).toBeInTheDocument();
expect(screen.getByText('experim...')).toBeInTheDocument();
// These should NOT be visible initially (3rd+ tags)
expect(screen.queryByText('api-cal...')).not.toBeInTheDocument();
expect(screen.queryByText('testing...')).not.toBeInTheDocument();
});
it('should show expand/collapse arrows for keys with more than 2 tags', () => {
// Production Key has 3 tags, so it should have an expand arrow
// Look for the chevron down icon (expand button)
const expandButtons = screen.getAllByRole('button');
const expandButton = expandButtons.find(button =>
button.getAttribute('title') === 'Show all tags'
);
expect(expandButton).toBeInTheDocument();
});
it('should show all tags when expanded', async () => {
// Find and click the expand button for Production Key (has 3 tags)
const expandButtons = screen.getAllByRole('button');
const expandButton = expandButtons.find(button =>
button.getAttribute('title') === 'Show all tags'
);
if (expandButton) {
fireEvent.click(expandButton);
// Now all tags should be visible
expect(screen.getByText('api-cal...')).toBeInTheDocument();
}
});
it('should show tooltip on hover with tag information', async () => {
// Hover over a tag to trigger tooltip
const tagElement = screen.getByText('high-vo...');
fireEvent.mouseOver(tagElement);
// Check that tooltip content appears
// Note: The exact tooltip content depends on your tooltip implementation
// You might need to adjust this based on how Ant Design Tooltip renders
});
});
describe('Tag Spend Formatting', () => {
it('should handle high spend amounts', () => {
renderWithProviders(<TopKeyView {...mockProps} topKeys={mockKeysWithTags} showTags={true} />);
// Test that high spend amounts are displayed
// This would require checking tooltip content or finding a way to access the formatted values
expect(screen.getByText('high-vo...')).toBeInTheDocument();
});
it('should handle micro spend amounts', () => {
renderWithProviders(<TopKeyView {...mockProps} topKeys={mockKeysWithTags} showTags={true} />);
// Test that very small amounts are displayed (only the top 2 tags are visible by default)
// product... has usage: 0.005 (<$0.01)
expect(screen.getByText('product...')).toBeInTheDocument();
// dev... has usage: 0.002 (<$0.01)
expect(screen.getByText('dev...')).toBeInTheDocument();
// experimental... has usage: 0.001 (<$0.01)
expect(screen.getByText('experim...')).toBeInTheDocument();
});
});
describe('Edge Cases', () => {
it('should handle keys with no tags', () => {
const keysWithoutTags = [
{
api_key: 'key-no-tags',
key_alias: 'No Tags Key',
tags: [],
spend: 10.00
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={keysWithoutTags} showTags={true} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should handle keys with undefined tags', () => {
const keysWithUndefinedTags = [
{
api_key: 'key-undefined-tags',
key_alias: 'Undefined Tags Key',
tags: undefined,
spend: 5.00
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={keysWithUndefinedTags} showTags={true} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should handle keys with null tags', () => {
const keysWithNullTags = [
{
api_key: 'key-null-tags',
key_alias: 'Null Tags Key',
tags: null,
spend: 3.00
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={keysWithNullTags} showTags={true} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
});
describe('Tag Truncation', () => {
it('should truncate long tag names to 7 characters', () => {
const keysWithLongTags = [
{
api_key: 'key-long-tags',
key_alias: 'Long Tags Key',
tags: [
{ tag: 'very-long-tag-name', usage: 10.00 } as TagUsage,
{ tag: 'short', usage: 5.00 } as TagUsage,
],
spend: 15.00
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={keysWithLongTags} showTags={true} />);
// Should show truncated version
expect(screen.getByText('very-lo...')).toBeInTheDocument();
// Short tags should still be truncated (all tags get ...)
expect(screen.getByText('short...')).toBeInTheDocument();
});
});
describe('Multiple Keys with Different Tag Spend Patterns', () => {
it('should handle mixed spend patterns across multiple keys', () => {
const mixedSpendKeys = [
{
api_key: 'key-mixed-1',
key_alias: 'Mixed Key 1',
tags: [
{ tag: 'expensive', usage: 999.99 } as TagUsage,
{ tag: 'cheap', usage: 0.001 } as TagUsage,
],
spend: 1000.00
},
{
api_key: 'key-mixed-2',
key_alias: 'Mixed Key 2',
tags: [
{ tag: 'moderate', usage: 50.00 } as TagUsage,
{ tag: 'tiny', usage: 0.005 } as TagUsage,
],
spend: 50.01
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={mixedSpendKeys} showTags={true} />);
// Verify that all tag types are displayed
expect(screen.getByText('expensi...')).toBeInTheDocument();
expect(screen.getByText('cheap...')).toBeInTheDocument();
expect(screen.getByText('moderat...')).toBeInTheDocument();
expect(screen.getByText('tiny...')).toBeInTheDocument();
});
});
describe('Table Structure', () => {
it('should render table with correct headers when showTags is true', () => {
renderWithProviders(<TopKeyView {...mockProps} showTags={true} />);
expect(screen.getByText('Key ID')).toBeInTheDocument();
expect(screen.getByText('Key Alias')).toBeInTheDocument();
expect(screen.getByText('Tags')).toBeInTheDocument();
expect(screen.getByText('Spend (USD)')).toBeInTheDocument();
});
it('should render table with correct headers when showTags is false', () => {
renderWithProviders(<TopKeyView {...mockProps} showTags={false} />);
expect(screen.getByText('Key ID')).toBeInTheDocument();
expect(screen.getByText('Key Alias')).toBeInTheDocument();
expect(screen.queryByText('Tags')).not.toBeInTheDocument();
expect(screen.getByText('Spend (USD)')).toBeInTheDocument();
});
});
describe('Key Data Display', () => {
it('should display key information correctly', () => {
const simpleKeys = [
{
api_key: 'test-key-123',
key_alias: 'Test Key',
tags: [],
spend: 25.50
}
];
renderWithProviders(<TopKeyView {...mockProps} topKeys={simpleKeys} showTags={true} />);
// Check that key alias is displayed
expect(screen.getByText('Test Key')).toBeInTheDocument();
// Check that spend is formatted correctly
expect(screen.getByText('$25.50')).toBeInTheDocument();
});
});
});

View file

@ -14,4 +14,11 @@ export default defineConfig({
'@': resolve(__dirname, 'src'),
},
},
define: {
'import.meta.vitest': 'undefined',
},
esbuild: {
jsx: 'automatic',
jsxImportSource: 'react',
},
})