From fec3b553139aa62110c375231246dd3638d9f6a2 Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 12:37:07 -0700 Subject: [PATCH 1/7] bringing tags into api top api keys table --- .../src/components/entity_usage.tsx | 21 ++++++++++++++++++- .../src/components/new_usage.tsx | 4 ++++ .../src/components/top_key_view.tsx | 5 +++++ .../src/components/usage/types.ts | 1 + 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index 7b910dab57b..ee44fd58290 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -175,8 +175,24 @@ const EntityUsage: React.FC = ({ }; 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]: string[] }, entity) => { + const {api_key_breakdown} = entities[entity]; + Object.keys(api_key_breakdown).forEach((key) => { + if (acc[key]) { + acc[key].push(entity); + } else { + acc[key] = [entity]; + } + }) + return acc; + },{}) + console.log('debugTags',{tagDictionary}) Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { if (!keySpend[key]) { keySpend[key] = { @@ -193,9 +209,11 @@ const EntityUsage: React.FC = ({ }, 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 +236,7 @@ const EntityUsage: React.FC = ({ .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) diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index e17c2d78be3..10dc306c713 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -269,6 +269,7 @@ const NewUsagePage: React.FC = ({ 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 = ({ 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) diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index 3db72123400..1c87807d81c 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -88,6 +88,11 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u accessorKey: "key_alias", cell: (info: any) => info.getValue() || "-", }, + { + header: "Tags", + accessorKey: "tags", + cell: (info: any) => info.getValue() || "-", + }, { header: "Spend (USD)", accessorKey: "spend", diff --git a/ui/litellm-dashboard/src/components/usage/types.ts b/ui/litellm-dashboard/src/components/usage/types.ts index 5d0779246cc..3848ba2e18b 100644 --- a/ui/litellm-dashboard/src/components/usage/types.ts +++ b/ui/litellm-dashboard/src/components/usage/types.ts @@ -39,6 +39,7 @@ export interface KeyMetricWithMetadata { export interface KeyMetadata { key_alias: string | null team_id: string | null + tags?: string[] } export interface TopApiKeyData { From 7c5f789b67d90770edf33585a91b02326f1dbba0 Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 13:49:01 -0700 Subject: [PATCH 2/7] styling tags, only showing tag column on tags tab --- .../src/components/entity_usage.tsx | 1 + .../src/components/top_key_view.tsx | 48 ++++++++++++++----- .../src/components/view_logs/table.tsx | 2 +- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index ee44fd58290..ca6615303e9 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -642,6 +642,7 @@ const EntityUsage: React.FC = ({ userRole={userRole} teams={null} premiumUser={premiumUser} + showTags={entityType === "tag"} /> diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index 1c87807d81c..43565330144 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -15,9 +15,10 @@ interface TopKeyViewProps { userRole: string | null teams: any[] | null premiumUser: boolean + showTags?: boolean } -const TopKeyView: React.FC = ({ topKeys, accessToken, userID, userRole, teams, premiumUser }) => { +const TopKeyView: React.FC = ({ topKeys, accessToken, userID, userRole, teams, premiumUser, showTags = false }) => { const [isModalOpen, setIsModalOpen] = useState(false) const [selectedKey, setSelectedKey] = useState(null) const [keyData, setKeyData] = useState(undefined) @@ -64,7 +65,7 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u }, [isModalOpen]) // Define columns for the table view - const columns = [ + const baseColumns = [ { header: "Key ID", accessorKey: "api_key", @@ -88,18 +89,41 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u accessorKey: "key_alias", cell: (info: any) => info.getValue() || "-", }, - { - header: "Tags", - accessorKey: "tags", - 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 string[] | undefined; + if (!tags || tags.length === 0) { + return "-"; + } + + return ( +
+ {tags.map((tag, index) => ( + + + {tag.slice(0, 7)}... + + + ))} +
+ ); + } + } + + const spendColumn = { + header: "Spend (USD)", + accessorKey: "spend", + cell: (info: any) => `$${formatNumberWithCommas(info.getValue(), 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 || "-", diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index c4f620e4413..b49ef30e83f 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -53,7 +53,7 @@ export function DataTable({ return (
- +
{table.getHeaderGroups().map((headerGroup) => ( From 4d340fa48f6f488006d12b65d009bc40ee489d5f Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 15:34:09 -0700 Subject: [PATCH 3/7] spend data in tags tooltip --- .../src/components/entity_usage.tsx | 9 ++++--- .../src/components/top_key_view.tsx | 27 +++++++++++++------ .../src/components/usage/types.ts | 7 ++++- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index ca6615303e9..f7617b304b1 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -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"; @@ -181,13 +181,14 @@ const EntityUsage: React.FC = ({ const {breakdown} = day; const {entities} = breakdown; console.log('debugTags',{entities}) - const tagDictionary = Object.keys(entities).reduce((acc: { [key: string]: string[] }, entity) => { + 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(entity); + acc[key].push(tagUsage); } else { - acc[key] = [entity]; + acc[key] = [tagUsage]; } }) return acc; diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index 43565330144..2e793ffe28e 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -7,6 +7,7 @@ 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" interface TopKeyViewProps { topKeys: any[] @@ -95,20 +96,30 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u header: "Tags", accessorKey: "tags", cell: (info: any) => { - const tags = info.getValue() as string[] | undefined; + const tags = info.getValue() as TagUsage[] | undefined; if (!tags || tags.length === 0) { return "-"; } return (
- {tags.map((tag, index) => ( - - - {tag.slice(0, 7)}... - - - ))} + {tags + .sort((a, b) => b.usage - a.usage) + .map((tag, index) => ( + +
TAG NAME: {tag.tag}
+
SPEND: {tag.usage > 0 && tag.usage < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(tag.usage, 2)}`}
+
+ } + > + + {tag.tag.slice(0, 7)}... + + + ))} ); } diff --git a/ui/litellm-dashboard/src/components/usage/types.ts b/ui/litellm-dashboard/src/components/usage/types.ts index 3848ba2e18b..b7c5ecb9ae7 100644 --- a/ui/litellm-dashboard/src/components/usage/types.ts +++ b/ui/litellm-dashboard/src/components/usage/types.ts @@ -39,7 +39,7 @@ export interface KeyMetricWithMetadata { export interface KeyMetadata { key_alias: string | null team_id: string | null - tags?: string[] + tags?: {tag:string,usage:number}[] } export interface TopApiKeyData { @@ -88,3 +88,8 @@ export interface EntityMetricWithMetadata { metrics: SpendMetrics metadata: EntityMetadata } + +export interface TagUsage { + tag: string + usage: number +} From a91a7e7750751ff8478c9c4af62ebd91f04933c0 Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 16:17:02 -0700 Subject: [PATCH 4/7] added testing file for api keys dashboard --- ui/litellm-dashboard/package-lock.json | 196 ++++++++++--- ui/litellm-dashboard/package.json | 1 + .../tests/top_key_view.test.tsx | 270 ++++++++++++++++++ ui/litellm-dashboard/vitest.config.ts | 2 + 4 files changed, 433 insertions(+), 36 deletions(-) create mode 100644 ui/litellm-dashboard/tests/top_key_view.test.tsx diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 2ff7e47a981..2301b806348 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -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" @@ -4208,6 +4249,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 +4815,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 +5533,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 +6496,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 +18990,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", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index c7546d3612c..30c35591d6d 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -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", diff --git a/ui/litellm-dashboard/tests/top_key_view.test.tsx b/ui/litellm-dashboard/tests/top_key_view.test.tsx new file mode 100644 index 00000000000..6406a64d847 --- /dev/null +++ b/ui/litellm-dashboard/tests/top_key_view.test.tsx @@ -0,0 +1,270 @@ +import React from 'react'; +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(); + expect(screen.queryByText('Tags')).not.toBeInTheDocument(); + }); + + it('should show tags column when showTags is true', () => { + renderWithProviders(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + }); + }); + + describe('Tags Display and Sorting', () => { + beforeEach(() => { + renderWithProviders(); + }); + + 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 all expected tag pills', () => { + // Verify all tag pills are rendered + const expectedTags = [ + 'product...', 'high-vo...', 'api-cal...', // Production Key tags + 'staging...', 'testing...', 'develop...', // Staging Key tags + 'dev...', 'experim...' // Development Key tags + ]; + + expectedTags.forEach(tag => { + expect(screen.getByText(tag)).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(); + + // 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(); + + // Test that very small amounts are displayed + expect(screen.getByText('product...')).toBeInTheDocument(); + expect(screen.getByText('api-cal...')).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(); + 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(); + 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(); + 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(); + + // 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(); + + // 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(); + + 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(); + + 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(); + + // Check that key alias is displayed + expect(screen.getByText('Test Key')).toBeInTheDocument(); + + // Check that spend is formatted correctly + expect(screen.getByText('$25.50')).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/litellm-dashboard/vitest.config.ts b/ui/litellm-dashboard/vitest.config.ts index d24c0d7f2a5..53e5a6fc828 100644 --- a/ui/litellm-dashboard/vitest.config.ts +++ b/ui/litellm-dashboard/vitest.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config' import { resolve } from "path" +import react from '@vitejs/plugin-react' export default defineConfig({ + plugins: [react()], test: { environment: 'jsdom', setupFiles: ['tests/setupTests.ts'], From 8760276918249111fa52654a4dceda056c25c155 Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 16:45:36 -0700 Subject: [PATCH 5/7] dropdown for more than two tags --- ui/litellm-dashboard/package-lock.json | 1 + .../src/components/top_key_view.tsx | 46 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 2301b806348..9ab39c57bfd 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -4092,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" } diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index 2e793ffe28e..39e66f7f77c 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -8,6 +8,7 @@ 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[] @@ -24,6 +25,19 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u const [selectedKey, setSelectedKey] = useState(null) const [keyData, setKeyData] = useState(undefined) const [viewMode, setViewMode] = useState<"chart" | "table">("table") + const [expandedTags, setExpandedTags] = useState>(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 @@ -97,29 +111,49 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u 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 (
- {tags - .sort((a, b) => b.usage - a.usage) - .map((tag, index) => ( +
+ {displayTags.map((tag, index) => ( -
TAG NAME: {tag.tag}
-
SPEND: {tag.usage > 0 && tag.usage < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(tag.usage, 2)}`}
+
Tag Name: {tag.tag}
+
Spend: {tag.usage > 0 && tag.usage < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(tag.usage, 2)}`}
} > - + {tag.tag.slice(0, 7)}... ))} + {hasMoreTags && ( + + )} +
); } From d303bf6743a276bca6faf0cd972d2fe6136a71b8 Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 16:55:36 -0700 Subject: [PATCH 6/7] port less than 1 cent to spend column --- ui/litellm-dashboard/src/components/top_key_view.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/top_key_view.tsx b/ui/litellm-dashboard/src/components/top_key_view.tsx index 39e66f7f77c..65fefbc9069 100644 --- a/ui/litellm-dashboard/src/components/top_key_view.tsx +++ b/ui/litellm-dashboard/src/components/top_key_view.tsx @@ -162,7 +162,10 @@ const TopKeyView: React.FC = ({ topKeys, accessToken, userID, u const spendColumn = { header: "Spend (USD)", accessorKey: "spend", - cell: (info: any) => `$${formatNumberWithCommas(info.getValue(), 2)}`, + cell: (info: any) => { + const value = info.getValue(); + return value > 0 && value < 0.01 ? '<$0.01' : `$${formatNumberWithCommas(value, 2)}`; + }, } const columns = showTags From 9a27eb5d0cae900b19a0b9f672cf0943b122d57c Mon Sep 17 00:00:00 2001 From: DrQuacks Date: Thu, 2 Oct 2025 18:32:26 -0700 Subject: [PATCH 7/7] edited tests to allow build --- .../tests/top_key_view.test.tsx | 62 +++++++++++++++---- ui/litellm-dashboard/vitest.config.ts | 9 ++- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/ui/litellm-dashboard/tests/top_key_view.test.tsx b/ui/litellm-dashboard/tests/top_key_view.test.tsx index 6406a64d847..c96edcd0b2f 100644 --- a/ui/litellm-dashboard/tests/top_key_view.test.tsx +++ b/ui/litellm-dashboard/tests/top_key_view.test.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderWithProviders, screen, fireEvent } from './test-utils'; import TopKeyView from '../src/components/top_key_view'; @@ -85,17 +84,48 @@ describe('TopKeyView', () => { expect(screen.getByText('staging...')).toBeInTheDocument(); }); - it('should display all expected tag pills', () => { - // Verify all tag pills are rendered - const expectedTags = [ - 'product...', 'high-vo...', 'api-cal...', // Production Key tags - 'staging...', 'testing...', 'develop...', // Staging Key tags - 'dev...', 'experim...' // Development Key tags - ]; + 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(); - expectedTags.forEach(tag => { - expect(screen.getByText(tag)).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 () => { @@ -121,9 +151,15 @@ describe('TopKeyView', () => { it('should handle micro spend amounts', () => { renderWithProviders(); - // Test that very small amounts are displayed + // 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(); - expect(screen.getByText('api-cal...')).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(); }); }); diff --git a/ui/litellm-dashboard/vitest.config.ts b/ui/litellm-dashboard/vitest.config.ts index 53e5a6fc828..69b65c5e199 100644 --- a/ui/litellm-dashboard/vitest.config.ts +++ b/ui/litellm-dashboard/vitest.config.ts @@ -1,9 +1,7 @@ import { defineConfig } from 'vitest/config' import { resolve } from "path" -import react from '@vitejs/plugin-react' export default defineConfig({ - plugins: [react()], test: { environment: 'jsdom', setupFiles: ['tests/setupTests.ts'], @@ -16,4 +14,11 @@ export default defineConfig({ '@': resolve(__dirname, 'src'), }, }, + define: { + 'import.meta.vitest': 'undefined', + }, + esbuild: { + jsx: 'automatic', + jsxImportSource: 'react', + }, })