diff --git a/package.json b/package.json index 65b0f93b..3419348a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@ant-design/icons": "^6.1.0", "@ant-design/pro-components": "3.1.0-0", "@braintree/sanitize-url": "^7.1.1", - "@gpustack/core-ui": "^1.0.5", + "@gpustack/core-ui": "^1.0.6", "@huggingface/gguf": "^0.1.7", "@huggingface/hub": "^0.15.1", "@huggingface/tasks": "^0.11.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b53aea8..c1b59659 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ dependencies: specifier: ^7.1.1 version: 7.1.1 '@gpustack/core-ui': - specifier: ^1.0.5 - version: 1.0.5(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0) + specifier: ^1.0.6 + version: 1.0.6(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0) '@huggingface/gguf': specifier: ^0.1.7 version: 0.1.18 @@ -2905,8 +2905,8 @@ packages: resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz} deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package - /@gpustack/core-ui@1.0.5(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0): - resolution: {integrity: sha512-IFFKXi1xIY6WWySasmKlRip/74eX/yb/iNH1iQ1sigzMWEV3j4V3VQvtBOUxsBxXmRViBSRrklIa0KDjtCwneQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.5.tgz} + /@gpustack/core-ui@1.0.6(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0): + resolution: {integrity: sha512-P7xs4Dh8dUKpN6YO9hNHgZUdEQvqU7TRMw+yJR5/SiIBAn04bhOZWoBMWR84TcSsT9pEt3IkTDIEmtBPtoQVnw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.6.tgz} peerDependencies: '@ant-design/icons': '>=6.0.0' '@monaco-editor/react': ^4.6.0 diff --git a/src/atoms/dashboard.ts b/src/atoms/dashboard.ts new file mode 100644 index 00000000..babcae03 --- /dev/null +++ b/src/atoms/dashboard.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai'; + +export const activeModelsAtom = atom([]); diff --git a/src/locales/en-US/dashboard.ts b/src/locales/en-US/dashboard.ts index 92998ccc..279b8123 100644 --- a/src/locales/en-US/dashboard.ts +++ b/src/locales/en-US/dashboard.ts @@ -20,6 +20,11 @@ export default { 'dashboard.tokens': 'Token Usage', 'dashboard.topusers': 'Top Users', 'dashboard.activeModels': 'Active Models', + 'dashboard.activeUsers': 'Active Users', + 'dashboard.tokenUsageByModel': 'Token Usage by Model', + 'dashboard.apiRequestsByModel': 'API Requests by Model', + 'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User', + 'dashboard.topTokenUsageByApiKey': 'Top 10 Token Usage by API Key', 'dashboard.runninginstances': 'Running Instances', 'dashboard.activeModels.name': 'Model Name', 'dashboard.allocatevram': 'Allocated VRAM / RAM', diff --git a/src/locales/ja-JP/dashboard.ts b/src/locales/ja-JP/dashboard.ts index 63ee9aa3..2db596f3 100644 --- a/src/locales/ja-JP/dashboard.ts +++ b/src/locales/ja-JP/dashboard.ts @@ -19,6 +19,11 @@ export default { 'dashboard.tokens': 'トークン使用量', 'dashboard.topusers': 'トップユーザー', 'dashboard.activeModels': 'アクティブなモデル', + 'dashboard.activeUsers': 'アクティブユーザー', + 'dashboard.tokenUsageByModel': 'モデル別トークン使用量', + 'dashboard.apiRequestsByModel': 'モデル別APIリクエスト', + 'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10', + 'dashboard.topTokenUsageByApiKey': 'APIキー別トークン使用量トップ10', 'dashboard.runninginstances': '稼働中のインスタンス', 'dashboard.activeModels.name': 'モデル名', 'dashboard.allocatevram': '割り当て済みVRAM / メモリ', diff --git a/src/locales/ru-RU/dashboard.ts b/src/locales/ru-RU/dashboard.ts index ee04bca1..f41c22bf 100644 --- a/src/locales/ru-RU/dashboard.ts +++ b/src/locales/ru-RU/dashboard.ts @@ -19,6 +19,11 @@ export default { 'dashboard.tokens': 'Использование токенов', 'dashboard.topusers': 'Топ пользователей', 'dashboard.activeModels': 'Активные модели', + 'dashboard.activeUsers': 'Активные пользователи', + 'dashboard.tokenUsageByModel': 'Использование токенов по моделям', + 'dashboard.apiRequestsByModel': 'API-запросы по моделям', + 'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам', + 'dashboard.topTokenUsageByApiKey': 'Топ-10 API-ключей по токенам', 'dashboard.runninginstances': 'Запущенные инстансы', 'dashboard.activeModels.name': 'Название модели', 'dashboard.allocatevram': 'Выделено VRAM / ОЗУ', diff --git a/src/locales/tr-TR/dashboard.ts b/src/locales/tr-TR/dashboard.ts index eb0844cc..b68ae998 100644 --- a/src/locales/tr-TR/dashboard.ts +++ b/src/locales/tr-TR/dashboard.ts @@ -20,6 +20,12 @@ export default { 'dashboard.tokens': 'Token Kullanımı', 'dashboard.topusers': 'En Aktif Kullanıcılar', 'dashboard.activeModels': 'Aktif Modeller', + 'dashboard.activeUsers': 'Aktif Kullanıcılar', + 'dashboard.tokenUsageByModel': 'Modele Göre Token Kullanımı', + 'dashboard.apiRequestsByModel': 'Modele Göre API İstekleri', + 'dashboard.topTokenUsageByUser': 'Kullanıcıya Göre İlk 10 Token Kullanımı', + 'dashboard.topTokenUsageByApiKey': + 'API Anahtarına Göre İlk 10 Token Kullanımı', 'dashboard.runninginstances': 'Çalışan Örnekler', 'dashboard.activeModels.name': 'Model Adı', 'dashboard.allocatevram': 'Ayrılan VRAM / RAM', diff --git a/src/locales/zh-CN/dashboard.ts b/src/locales/zh-CN/dashboard.ts index 09a3b04f..79d91634 100644 --- a/src/locales/zh-CN/dashboard.ts +++ b/src/locales/zh-CN/dashboard.ts @@ -20,6 +20,11 @@ export default { 'dashboard.tokens': 'Token 使用量', 'dashboard.topusers': '用户排行', 'dashboard.activeModels': '活跃模型', + 'dashboard.activeUsers': '活跃用户', + 'dashboard.tokenUsageByModel': '按模型统计 Token 使用量', + 'dashboard.apiRequestsByModel': '按模型统计 API 请求数', + 'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10', + 'dashboard.topTokenUsageByApiKey': 'API 密钥 Token 使用量 Top 10', 'dashboard.activeModels.name': '模型名称', 'dashboard.runninginstances': '运行实例', 'dashboard.allocatevram': '已分配显存 / 内存', diff --git a/src/pages/_components/pie-chart/index.tsx b/src/pages/_components/pie-chart/index.tsx new file mode 100644 index 00000000..c1ac9476 --- /dev/null +++ b/src/pages/_components/pie-chart/index.tsx @@ -0,0 +1,192 @@ +import { Chart } from '@gpustack/core-ui'; +import { formatLargeNumber } from '@gpustack/core-ui/utils'; +import { Empty, Spin, theme } from 'antd'; +import React, { useMemo, useRef } from 'react'; +import { generateCoolColors } from '../bar-chart'; + +export interface PieChartItem { + name: string; + value: number; +} + +interface PieChartProps { + data: PieChartItem[]; + height?: number | string; + width?: number | string; + loading?: boolean; + colorOffset?: number; + total?: number; + totalLabel?: string; +} + +const PieChart: React.FC = ({ + data, + height = 300, + width = '100%', + loading = false, + colorOffset = 0, + total, + totalLabel +}) => { + const { token } = theme.useToken(); + const chartRef = useRef<{ chart: any } | null>(null); + + const colors = useMemo(() => { + const generatedColors = generateCoolColors(data.length + colorOffset); + return generatedColors.slice(colorOffset); + }, [colorOffset, data.length]); + + const options = useMemo( + () => ({ + color: colors, + tooltip: { + trigger: 'item', + backgroundColor: token.colorBgElevated, + borderColor: 'transparent', + formatter: (params: any) => { + return `
+ + + + ${params.name}: + + ${formatLargeNumber(params.value)} + +
`; + } + }, + legend: { + type: 'scroll', + orient: 'vertical', + right: 0, + top: 18, + bottom: 18, + itemWidth: 8, + itemHeight: 8, + itemGap: 12, + textStyle: { + color: token.colorTextTertiary, + overflow: 'truncate', + width: 180 + }, + pageTextStyle: { color: token.colorTextTertiary }, + pageIconColor: token.colorTextTertiary, + pageIconInactiveColor: token.colorTextDisabled, + data: data.map((item) => item.name), + show: data.length > 0 + }, + series: [ + { + type: 'pie', + radius: ['52%', '72%'], + center: ['34%', '50%'], + avoidLabelOverlap: true, + label: { + show: false + }, + emphasis: { + label: { + show: true, + formatter: (params: any) => `${params.percent}%`, + fontSize: 16, + fontWeight: 600, + color: token.colorText + } + }, + labelLine: { + show: false + }, + data + } + ] + }), + [colors, data, token] + ); + + if (!data.length) { + return ( +
+ {loading ? ( + + ) : ( + + )} +
+ ); + } + + const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0); + + return ( +
+ +
+ + {formatLargeNumber(displayTotal)} + + {totalLabel && ( + + {totalLabel} + + )} +
+ {loading && ( +
+ +
+ )} +
+ ); +}; + +export default PieChart; diff --git a/src/pages/benchmark/components/summary/instance.tsx b/src/pages/benchmark/components/summary/instance.tsx index 7ab843d5..55d5d9cd 100644 --- a/src/pages/benchmark/components/summary/instance.tsx +++ b/src/pages/benchmark/components/summary/instance.tsx @@ -80,17 +80,17 @@ const Instance: React.FC = () => { { key: '1', label: intl.formatMessage({ - id: 'models.instance.params.configured' + id: 'models.form.backend_parameters' }), children: renderParams(instanceData?.backend_parameters || []) }, - { - key: '1-1', - label: intl.formatMessage({ - id: 'models.instance.params.autoInjected' - }), - children: renderParams(instanceData?.injected_backend_parameters || []) - }, + // { + // key: '1-1', + // label: intl.formatMessage({ + // id: 'models.instance.params.autoInjected' + // }), + // children: renderParams(instanceData?.injected_backend_parameters || []) + // }, { key: '3', label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }), diff --git a/src/pages/dashboard/components/active-table.tsx b/src/pages/dashboard/components/active-table.tsx index b070f531..88504119 100644 --- a/src/pages/dashboard/components/active-table.tsx +++ b/src/pages/dashboard/components/active-table.tsx @@ -1,9 +1,11 @@ +import { activeModelsAtom } from '@/atoms/dashboard'; import { modelCategoriesMap } from '@/pages/llmodels/config'; import { convertFileSize } from '@/utils'; import { AutoTooltip, PageTools } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Col, Row, Table } from 'antd'; -import { useContext } from 'react'; +import { useSetAtom } from 'jotai'; +import { useContext, useEffect } from 'react'; import { DashboardContext } from '../config/dashboard-context'; const NACategories = [ modelCategoriesMap.llm, @@ -14,6 +16,12 @@ const NACategories = [ const ActiveTable = () => { const intl = useIntl(); const data = useContext(DashboardContext).active_models || []; + const setActiveModels = useSetAtom(activeModelsAtom); + + useEffect(() => { + setActiveModels(data); + }, [data, setActiveModels]); + const modelColumns = [ { title: intl.formatMessage({ id: 'common.table.name' }), diff --git a/src/pages/dashboard/components/new-usage.tsx b/src/pages/dashboard/components/new-usage.tsx new file mode 100644 index 00000000..5ad23ae1 --- /dev/null +++ b/src/pages/dashboard/components/new-usage.tsx @@ -0,0 +1,157 @@ +import { activeModelsAtom } from '@/atoms/dashboard'; +import useQueryBreakdownList from '@/pages/usage/services/use-query-breakdown-list'; +import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; +import { formatLargeNumber } from '@/utils'; +import { PageTools } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Col, Row } from 'antd'; +import dayjs from 'dayjs'; +import { useAtomValue } from 'jotai'; +import { useContext, useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { + baseColorMap, + DashboardUsageCommonParams, + getUsageSummary +} from '../config'; +import { DashboardContext } from '../config/dashboard-context'; +import ApiRequestsByModel from './usage-charts/api-requests-by-model'; +import TokenUsageByModel from './usage-charts/token-usage-by-model'; +import TopTokenUsageByApiKey from './usage-charts/top-token-usage-by-api-key'; +import TopTokenUsageByUser from './usage-charts/top-token-usage-by-user'; + +const Section = styled.div` + margin-top: 16px; +`; + +const NewUsage = () => { + const intl = useIntl(); + const activeModelsFromAtom = useAtomValue(activeModelsAtom); + const { active_models: activeModelsFromContext = [] } = + useContext(DashboardContext); + const tokenByModelQuery = useQueryTimeSeriesData({ + key: 'tokenUsageByModelData' + }); + const activeUsersQuery = useQueryBreakdownList({ + key: 'dashboardActiveUsersData' + }); + + const dateRange = useMemo( + () => ({ + start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'), + end_date: dayjs().format('YYYY-MM-DD') + }), + [] + ); + + const commonParams = useMemo( + () => ({ + ...dateRange, + scope: 'all', + granularity: 'day', + filters: {} + }), + [dateRange] + ); + + const activeModels = activeModelsFromAtom.length + ? activeModelsFromAtom + : activeModelsFromContext; + + useEffect(() => { + const fetchData = async () => { + await Promise.all([ + tokenByModelQuery.fetchData({ + ...commonParams, + metric: 'total_tokens', + group_by: ['date', 'model'], + sort_by: '-total_tokens' + }), + activeUsersQuery.fetchData({ + ...dateRange, + scope: 'all', + group_by: ['user'], + page: 1, + perPage: 10, + sort_by: '-total_tokens', + filters: {} + }) + ]); + }; + + fetchData().catch(() => {}); + }, [commonParams, dateRange]); + + const summaryCards = useMemo(() => { + const summary = getUsageSummary(tokenByModelQuery.detailData) || { + total_tokens: 0, + api_requests: 0 + }; + + return [ + { + label: formatLargeNumber(summary.total_tokens) as string, + value: intl.formatMessage({ id: 'dashboard.tokens' }), + color: baseColorMap.base + }, + { + label: formatLargeNumber(summary.api_requests) as string, + value: intl.formatMessage({ id: 'dashboard.apirequest' }), + color: baseColorMap.baseR1 + }, + { + label: formatLargeNumber(activeModels.length) as string, + value: intl.formatMessage({ id: 'dashboard.activeModels' }), + color: baseColorMap.baseR2 + }, + { + label: formatLargeNumber(activeUsersQuery.dataSource.total) as string, + value: intl.formatMessage({ id: 'dashboard.activeUsers' }), + color: baseColorMap.baseR3 + } + ]; + }, [ + activeModels.length, + activeUsersQuery.dataSource.total, + intl, + tokenByModelQuery.detailData + ]); + + return ( + <> + + {intl.formatMessage({ id: 'dashboard.usage' })} + + } + /> +
+ + + + + + + + +
+
+ + + + + + + + +
+ + ); +}; + +export default NewUsage; diff --git a/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx b/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx new file mode 100644 index 00000000..4ad486c8 --- /dev/null +++ b/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx @@ -0,0 +1,63 @@ +import PieChart from '@/pages/_components/pie-chart'; +import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; +import { useIntl } from '@umijs/max'; +import { useEffect, useMemo } from 'react'; +import { + DashboardUsageCommonParams, + getUsageSummary, + toUsagePieData, + usageChartHeight +} from '../../config'; +import UsageChartCard from './shared'; + +interface ApiRequestsByModelProps { + commonParams: DashboardUsageCommonParams; +} + +const ApiRequestsByModel: React.FC = ({ + commonParams +}) => { + const intl = useIntl(); + const query = useQueryTimeSeriesData({ + key: 'apiRequestsByModelData' + }); + + useEffect(() => { + query + .fetchData({ + ...commonParams, + metric: 'api_requests', + group_by: ['date', 'model'], + page: 1, + perPage: 10, + sort_by: '-api_requests' + }) + .catch(() => {}); + }, [commonParams]); + + const chartData = useMemo( + () => toUsagePieData(query.detailData, 'model', 'api_requests'), + [query.detailData] + ); + + const total = useMemo(() => { + const summary = getUsageSummary(query.detailData); + return Number(summary?.api_requests ?? 0); + }, [query.detailData]); + + return ( + + + + ); +}; + +export default ApiRequestsByModel; diff --git a/src/pages/dashboard/components/usage-charts/shared.tsx b/src/pages/dashboard/components/usage-charts/shared.tsx new file mode 100644 index 00000000..3b27b39b --- /dev/null +++ b/src/pages/dashboard/components/usage-charts/shared.tsx @@ -0,0 +1,42 @@ +import { CardWrapper } from '@gpustack/core-ui'; +import { Spin } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { usageChartCardHeight, usageChartHeight } from '../../config'; + +export const ChartTitle = styled.div` + font-weight: var(--font-weight-bold); + margin-bottom: 12px; +`; + +const UsageChartCard: React.FC<{ + title: React.ReactNode; + children: React.ReactNode; + height?: number | string; +}> = ({ title, children, height = usageChartCardHeight }) => { + return ( + + {title} + {children} + + ); +}; + +export default UsageChartCard; + +export const ChartLoadingBox: React.FC<{ height?: number | string }> = ({ + height = usageChartHeight +}) => { + return ( +
+ +
+ ); +}; diff --git a/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx b/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx new file mode 100644 index 00000000..1263d58c --- /dev/null +++ b/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx @@ -0,0 +1,48 @@ +import PieChart from '@/pages/_components/pie-chart'; +import { UsageBreakdownResponse } from '@/pages/usage/config/types'; +import { useIntl } from '@umijs/max'; +import { useMemo } from 'react'; +import { + getUsageSummary, + toUsagePieData, + usageChartHeight +} from '../../config'; +import UsageChartCard from './shared'; + +interface TokenUsageByModelProps { + data: UsageBreakdownResponse | null | undefined; + loading?: boolean; +} + +const TokenUsageByModel: React.FC = ({ + data, + loading = false +}) => { + const intl = useIntl(); + + const chartData = useMemo( + () => toUsagePieData(data, 'model', 'total_tokens'), + [data] + ); + + const total = useMemo(() => { + const summary = getUsageSummary(data); + return Number(summary?.total_tokens ?? 0); + }, [data]); + + return ( + + + + ); +}; + +export default TokenUsageByModel; diff --git a/src/pages/dashboard/components/usage-charts/top-token-usage-by-api-key.tsx b/src/pages/dashboard/components/usage-charts/top-token-usage-by-api-key.tsx new file mode 100644 index 00000000..a59b06a7 --- /dev/null +++ b/src/pages/dashboard/components/usage-charts/top-token-usage-by-api-key.tsx @@ -0,0 +1,76 @@ +import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; +import { HBarChart } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { useEffect, useMemo } from 'react'; +import { + baseColorMap, + DashboardUsageCommonParams, + toUsageRankData, + usageChartHeight +} from '../../config'; +import UsageChartCard, { ChartLoadingBox } from './shared'; + +interface TopTokenUsageByApiKeyProps { + commonParams: DashboardUsageCommonParams; + height?: number; +} + +const TopTokenUsageByApiKey: React.FC = ({ + commonParams, + height = usageChartHeight +}) => { + const intl = useIntl(); + const query = useQueryTimeSeriesData({ + key: 'topTokenUsageByApiKeyData' + }); + const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' }); + + useEffect(() => { + query + .fetchData({ + ...commonParams, + metric: 'total_tokens', + group_by: ['date', 'api_key'], + page: 1, + perPage: 10, + sort_by: '-total_tokens' + }) + .catch(() => {}); + }, [commonParams]); + + const rankData = useMemo(() => { + const data = toUsageRankData( + query.detailData, + 'api_key', + tokenUsageText, + baseColorMap.baseR3 + ); + return { + names: data.names.filter((name) => name !== '-'), + series: data.series.map((item) => ({ + ...item, + data: item.data.filter((point) => point.name !== '-') + })) + }; + }, [query.detailData, tokenUsageText]); + + return ( + + {query.loading && rankData.names.length === 0 ? ( + + ) : ( + + )} + + ); +}; + +export default TopTokenUsageByApiKey; diff --git a/src/pages/dashboard/components/usage-charts/top-token-usage-by-user.tsx b/src/pages/dashboard/components/usage-charts/top-token-usage-by-user.tsx new file mode 100644 index 00000000..1e1f02c5 --- /dev/null +++ b/src/pages/dashboard/components/usage-charts/top-token-usage-by-user.tsx @@ -0,0 +1,71 @@ +import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; +import { HBarChart } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { useEffect, useMemo } from 'react'; +import { + baseColorMap, + DashboardUsageCommonParams, + toUsageRankData, + usageChartHeight +} from '../../config'; +import UsageChartCard, { ChartLoadingBox } from './shared'; + +interface TopTokenUsageByUserProps { + commonParams: DashboardUsageCommonParams; + height?: number; +} + +const TopTokenUsageByUser: React.FC = ({ + commonParams, + height = usageChartHeight +}) => { + const intl = useIntl(); + const query = useQueryTimeSeriesData({ + key: 'topTokenUsageByUserData' + }); + const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' }); + + useEffect(() => { + query + .fetchData({ + ...commonParams, + metric: 'total_tokens', + group_by: ['date', 'user'], + page: 1, + perPage: 10, + sort_by: '-total_tokens' + }) + .catch(() => {}); + }, [commonParams]); + + const rankData = useMemo( + () => + toUsageRankData( + query.detailData, + 'user', + tokenUsageText, + baseColorMap.base + ), + [query.detailData, tokenUsageText] + ); + + return ( + + {query.loading && rankData.names.length === 0 ? ( + + ) : ( + + )} + + ); +}; + +export default TopTokenUsageByUser; diff --git a/src/pages/dashboard/components/usage.tsx b/src/pages/dashboard/components/usage.tsx index 33cabc80..aa850b87 100644 --- a/src/pages/dashboard/components/usage.tsx +++ b/src/pages/dashboard/components/usage.tsx @@ -1,18 +1,7 @@ -import breakpoints from '@/config/breakpoints'; -import useWindowResize from '@/hooks/use-window-resize'; -import { useEffect, useState } from 'react'; -import UserInner from './usage-inner'; +import NewUsage from './new-usage'; const Usage = () => { - const { - size: { width } - } = useWindowResize(); - const [maxWdith, setMaxWidth] = useState(breakpoints.xl); - useEffect(() => { - setMaxWidth(width); - }, [width]); - - return ; + return ; }; export default Usage; diff --git a/src/pages/dashboard/config/index.ts b/src/pages/dashboard/config/index.ts index 0f8d93b3..d63bb04b 100644 --- a/src/pages/dashboard/config/index.ts +++ b/src/pages/dashboard/config/index.ts @@ -1,3 +1,8 @@ +import { + BreakdownItem, + UsageBreakdownResponse +} from '@/pages/usage/config/types'; + export const overviewConfigs = [ { key: 'cluster_count', @@ -35,3 +40,187 @@ export const baseColorMap = { baseR2: 'rgba(48,0,255,0.8)', baseR3: 'rgba(85,167,255,0.8)' }; + +export type UsageGroupBy = 'model' | 'user' | 'api_key'; +export type UsageMetric = 'total_tokens' | 'api_requests'; + +export interface UsageChartDatum { + name: string; + value: number; +} + +export interface DashboardUsageCommonParams { + start_date: string; + end_date: string; + scope: string; + granularity: string; + filters: Record; +} + +export const usageChartHeight = 300; +export const usageChartCardHeight = usageChartHeight + 52; + +export const buildUsageLabel = (item: BreakdownItem, groupBy: UsageGroupBy) => { + const rawItem = item as any; + const groupItem = rawItem[groupBy]; + const identityValue = groupItem?.identity?.value; + const groupValue = groupItem?.value; + + if (typeof groupItem === 'string') { + return groupItem; + } + + if (groupBy === 'model') { + const providerName = + identityValue?.provider_name || groupValue?.provider_name; + const modelName = + identityValue?.model_name || + groupValue?.model_name || + rawItem.model_name || + rawItem.model; + + if (providerName && modelName) { + return `${providerName}/${modelName}`; + } + + return groupItem?.label || modelName || '-'; + } + + return ( + groupItem?.label || + identityValue?.user_name || + identityValue?.api_key_name || + identityValue?.access_key || + groupValue?.user_name || + groupValue?.api_key_name || + groupValue?.access_key || + rawItem.user_name || + rawItem.api_key_name || + rawItem.access_key || + rawItem[groupBy] || + '-' + ); +}; + +export const getUsageResponseItems = ( + data: UsageBreakdownResponse | null | undefined +) => { + const response = data as any; + return response?.items || response?.data?.items || []; +}; + +export const getUsageResponseSeries = ( + data: UsageBreakdownResponse | null | undefined +) => { + const response = data as any; + return response?.series || response?.data?.series || []; +}; + +export const getUsageMetricValue = (item: any, metric: UsageMetric) => { + return Number( + item?.[metric] ?? + item?.metrics?.[metric] ?? + item?.summary?.[metric] ?? + item?.value ?? + 0 + ); +}; + +export const aggregateUsageByGroup = ( + data: UsageBreakdownResponse | null | undefined, + groupBy: UsageGroupBy, + metric: UsageMetric +): UsageChartDatum[] => { + const itemMap = new Map(); + const items = getUsageResponseItems(data); + + items.forEach((item: BreakdownItem) => { + const name = buildUsageLabel(item, groupBy); + itemMap.set( + name, + (itemMap.get(name) || 0) + getUsageMetricValue(item, metric) + ); + }); + + if (!items.length) { + getUsageResponseSeries(data).forEach((item: any) => { + const name = item.label || buildUsageLabel(item, groupBy); + const value = (item.timeline || []).reduce( + (sum: number, point: any) => { + return sum + getUsageMetricValue(point, metric); + }, + getUsageMetricValue(item, metric) + ); + + itemMap.set(name, (itemMap.get(name) || 0) + value); + }); + } + + if (!itemMap.size) { + return []; + } + + const hasPositiveValue = Array.from(itemMap.values()).some( + (value) => value > 0 + ); + + if (!hasPositiveValue) { + return []; + } + + return Array.from(itemMap.entries()) + .filter(([, value]) => value > 0) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value) + .slice(0, 10); +}; + +export const getUsageSummaryData = ( + dataList: Array +) => { + return dataList.find((item) => { + const summary = item?.summary || (item as any)?.data?.summary; + return summary; + }); +}; + +export const getUsageSummary = ( + data: UsageBreakdownResponse | null | undefined +) => { + return data?.summary || (data as any)?.data?.summary; +}; + +export const toUsagePieData = ( + data: UsageBreakdownResponse | null | undefined, + groupBy: UsageGroupBy, + metric: UsageMetric +): UsageChartDatum[] => { + return aggregateUsageByGroup(data, groupBy, metric); +}; + +export const toUsageRankData = ( + data: UsageBreakdownResponse | null | undefined, + groupBy: UsageGroupBy, + seriesName: string, + color: string +) => { + const items = aggregateUsageByGroup(data, groupBy, 'total_tokens'); + const names = items.map((item) => item.name); + + return { + names, + series: [ + { + name: seriesName, + color, + data: items.map((item) => ({ + name: item.name, + value: item.value, + itemStyle: { + borderRadius: [2, 2, 2, 2] + } + })) + } + ] + }; +}; diff --git a/src/pages/llmodels/components/instance-cells/injected-params-cell.tsx b/src/pages/llmodels/components/instance-cells/injected-params-cell.tsx index 6e7cf2cc..50358076 100644 --- a/src/pages/llmodels/components/instance-cells/injected-params-cell.tsx +++ b/src/pages/llmodels/components/instance-cells/injected-params-cell.tsx @@ -95,7 +95,7 @@ const InjectedParamsCell: React.FC = ({ if (!injectedBackendParameters.length) { return null; } - + return null; return ( -
+ {/*
-
+
*/} ) }, @@ -153,7 +152,7 @@ const useInstancesColumns = (options: { dataIndex: 'worker_id', render: (text, record) => (
- {renderWorkerCell(text, record)} + {record.worker_name} ({ fetchDetail: queryUsageTimeSeriesData, - key: 'timeSeriesData' + key: option?.key || 'timeSeriesData' }); return { detailData,