diff --git a/src/pages/usage/hooks/use-export-table.tsx b/src/pages/usage/hooks/use-export-table.tsx
index 82b90281..b68297c7 100644
--- a/src/pages/usage/hooks/use-export-table.tsx
+++ b/src/pages/usage/hooks/use-export-table.tsx
@@ -12,7 +12,11 @@ import useUsersColumns from './use-users-columns';
type ColumnLike = {
title: any;
- dataIndex: string | string[];
+ // Plugin-contributed columns may omit `dataIndex` — they render via a
+ // bound component and aren't tied to a single row field. The export
+ // path skips those entries; an exported sheet has no place for a
+ // computed cell anyway.
+ dataIndex?: string | string[];
key?: string;
};
diff --git a/src/pages/usage/hooks/use-models-columns.tsx b/src/pages/usage/hooks/use-models-columns.tsx
index 0f47f4ac..2c063a37 100644
--- a/src/pages/usage/hooks/use-models-columns.tsx
+++ b/src/pages/usage/hooks/use-models-columns.tsx
@@ -1,18 +1,50 @@
// columns.ts
+import { getGPUStackPlugin } from '@/plugins';
import { AutoTooltip } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Tag } from 'antd';
import { useMemo } from 'react';
import { BreakdownItem as ListItem } from '../config/types';
-const useModelsColumns = (): Array<{
- title: string;
- dataIndex: string | string[];
+// Plugin slot: enterprise plugins can contribute extra columns to the
+// usage breakdown Models table via `usage.modelsExtraColumns`. Each
+// entry renders a per-row cell keyed off `record.route.identity.current
+// .route_id`. `placement` decides where in the existing column order
+// the column lands (currently only `before-last-active` is supported,
+// which is the natural spot for per-route quota / usage indicators).
+export type UsageModelsPluginColumn = {
key: string;
-}> => {
+ titleId: string;
+ placement?: 'before-last-active';
+ render: (record: ListItem) => React.ReactNode;
+};
+
+type ColumnDef = {
+ title: React.ReactNode;
+ dataIndex?: string | string[];
+ key: string;
+ sorter?: boolean;
+ ellipsis?: { showTitle: boolean };
+ render?: (text: any, record: ListItem) => React.ReactNode;
+};
+
+const useModelsColumns = (): ColumnDef[] => {
const intl = useIntl();
return useMemo(() => {
+ const pluginColumns: UsageModelsPluginColumn[] =
+ getGPUStackPlugin()?.usage?.modelsExtraColumns ?? [];
+
+ const pluginCols: ColumnDef[] = pluginColumns.map((c) => ({
+ title: intl.formatMessage({ id: c.titleId }),
+ key: c.key,
+ render: (_text: any, record: ListItem) => c.render(record)
+ }));
+ const beforeLastActive = pluginCols.filter(
+ (_c, i) =>
+ (pluginColumns[i].placement ?? 'before-last-active') ===
+ 'before-last-active'
+ );
return [
{
title: intl.formatMessage({ id: 'common.table.name' }),
@@ -95,6 +127,7 @@ const useModelsColumns = (): Array<{
},
render: (text: number) => {text}
},
+ ...beforeLastActive,
{
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
dataIndex: 'last_active',
diff --git a/src/pages/usage/tables/models-table.tsx b/src/pages/usage/tables/models-table.tsx
index 3fc333cf..bb2f5bef 100644
--- a/src/pages/usage/tables/models-table.tsx
+++ b/src/pages/usage/tables/models-table.tsx
@@ -1,10 +1,11 @@
+import PluginExtraFields from '@/components/plugin-extra-fields';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import PageBox from '@/pages/_components/page-box';
import { IconFont, NoResult } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Table } from 'antd';
import _ from 'lodash';
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { BreakdownItem, FilterOptionType } from '../config/types';
import useModelsColumns from '../hooks/use-models-columns';
import useQueryBreakdownList from '../services/use-query-breakdown-list';
@@ -107,6 +108,22 @@ const Models: React.FC<{
scope
]);
+ // Route ids currently visible in the Models tab. Mirrored to the
+ // plugin slot below so enterprise plugins can bulk-fetch per-route
+ // data (e.g. the caller's quota / usage on each route) in one call
+ // rather than firing N round-trips from each cell. `refreshToken`
+ // bumps with every successful fetch so plugins re-pull even when the
+ // id set hasn't changed.
+ const pluginContext = useMemo(
+ () => ({
+ routeIds: (dataSource.dataList || [])
+ .map((item) => item.route?.identity?.current?.route_id)
+ .filter((id): id is number => id != null),
+ refreshToken: refreshKey
+ }),
+ [dataSource.dataList, refreshKey]
+ );
+
return (
<>
@@ -134,6 +151,11 @@ const Models: React.FC<{
}}
>
+ {/* Page-level data lifecycle for plugin-contributed extra
+ columns on this tab. Receives the visible route ids so the
+ plugin can bulk-fetch per-route data in one call; renders
+ nothing when no plugin is registered. */}
+
>
);
};