mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge pull request #15155 from BerriAI/litellm_dev_10_02_2025_p1
Litellm dev 10 02 2025 p1
This commit is contained in:
commit
1d548d7235
9 changed files with 593 additions and 46 deletions
197
ui/litellm-dashboard/package-lock.json
generated
197
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 || "-",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
306
ui/litellm-dashboard/tests/top_key_view.test.tsx
Normal file
306
ui/litellm-dashboard/tests/top_key_view.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -14,4 +14,11 @@ export default defineConfig({
|
|||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': 'undefined',
|
||||
},
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
jsxImportSource: 'react',
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue