[A2a gateway] Add agent cost tracking on UI (#17899)

* add CostConfigFields

* add CostConfigFields

* add output_cost_per_token

* refactor table

* add agent cost view
This commit is contained in:
Ishaan Jaff
2025-12-12 16:37:31 -08:00
committed by GitHub
parent b93a5f0ca6
commit bede40a90d
7 changed files with 320 additions and 63 deletions
@@ -28,6 +28,7 @@ export const AGENT_FORM_CONFIG: {
capabilities: SectionConfig;
optional: SectionConfig;
litellm: SectionConfig;
cost: SectionConfig;
} = {
basic: {
key: "basic",
@@ -146,6 +147,33 @@ export const AGENT_FORM_CONFIG: {
},
],
},
cost: {
key: "cost",
title: "Cost Configuration",
fields: [
{
name: "cost_per_query",
label: "Cost Per Query ($)",
type: "text",
placeholder: "0.0",
tooltip: "Fixed cost per query",
},
{
name: "input_cost_per_token",
label: "Input Cost Per Token ($)",
type: "text",
placeholder: "0.000001",
tooltip: "Cost per input token",
},
{
name: "output_cost_per_token",
label: "Output Cost Per Token ($)",
type: "text",
placeholder: "0.000002",
tooltip: "Cost per output token",
},
],
},
};
export const SKILL_FIELD_CONFIG = {
@@ -229,12 +257,16 @@ export const buildAgentDataFromForm = (values: any, existingAgent?: any) => {
},
};
// Only add litellm_params if there are values
if (values.model || values.make_public !== undefined) {
agentData.litellm_params = {
...(values.model && { model: values.model }),
...(values.make_public !== undefined && { make_public: values.make_public }),
};
const params: Record<string, any> = {};
if (values.model) params.model = values.model;
if (values.make_public !== undefined) params.make_public = values.make_public;
if (values.cost_per_query) params.cost_per_query = parseFloat(values.cost_per_query);
if (values.input_cost_per_token) params.input_cost_per_token = parseFloat(values.input_cost_per_token);
if (values.output_cost_per_token) params.output_cost_per_token = parseFloat(values.output_cost_per_token);
if (Object.keys(params).length > 0) {
agentData.litellm_params = params;
}
return agentData;
@@ -267,5 +299,8 @@ export const parseAgentForForm = (agent: any) => {
supportsAuthenticatedExtendedCard: agent.agent_card_params?.supportsAuthenticatedExtendedCard,
model: agent.litellm_params?.model,
make_public: agent.litellm_params?.make_public,
cost_per_query: agent.litellm_params?.cost_per_query,
input_cost_per_token: agent.litellm_params?.input_cost_per_token,
output_cost_per_token: agent.litellm_params?.output_cost_per_token,
};
};
@@ -0,0 +1,46 @@
import React from "react";
import { Title } from "@tremor/react";
import { Descriptions } from "antd";
import { Agent } from "./types";
interface AgentCostViewProps {
agent: Agent;
}
const AgentCostView: React.FC<AgentCostViewProps> = ({ agent }) => {
const params = agent.litellm_params;
if (
params?.cost_per_query === undefined &&
params?.input_cost_per_token === undefined &&
params?.output_cost_per_token === undefined
) {
return null;
}
return (
<div style={{ marginTop: 24 }}>
<Title>Cost Configuration</Title>
<Descriptions bordered column={1} style={{ marginTop: 16 }}>
{params.cost_per_query !== undefined && (
<Descriptions.Item label="Cost Per Query">
${params.cost_per_query}
</Descriptions.Item>
)}
{params.input_cost_per_token !== undefined && (
<Descriptions.Item label="Input Cost Per Token">
${params.input_cost_per_token}
</Descriptions.Item>
)}
{params.output_cost_per_token !== undefined && (
<Descriptions.Item label="Output Cost Per Token">
${params.output_cost_per_token}
</Descriptions.Item>
)}
</Descriptions>
</div>
);
};
export default AgentCostView;
@@ -4,6 +4,8 @@ import { Button as AntButton } from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import { AGENT_FORM_CONFIG, SKILL_FIELD_CONFIG } from "./agent_config";
import CostConfigFields from "./cost_config_fields";
const { Panel } = Collapse;
interface AgentFormFieldsProps {
@@ -154,6 +156,11 @@ const AgentFormFields: React.FC<AgentFormFieldsProps> = ({ showAgentName = true
))}
</Panel>
{/* Cost Configuration */}
<Panel header={AGENT_FORM_CONFIG.cost.title} key={AGENT_FORM_CONFIG.cost.key}>
<CostConfigFields />
</Panel>
{/* LiteLLM Parameters */}
<Panel header={AGENT_FORM_CONFIG.litellm.title} key={AGENT_FORM_CONFIG.litellm.key}>
{AGENT_FORM_CONFIG.litellm.fields.map((field) => (
@@ -6,6 +6,7 @@ import { getAgentInfo, patchAgentCall } from "../networking";
import { Agent } from "./types";
import AgentFormFields from "./agent_form_fields";
import { buildAgentDataFromForm, parseAgentForForm } from "./agent_config";
import AgentCostView from "./agent_cost_view";
interface AgentInfoViewProps {
agentId: string;
@@ -147,6 +148,8 @@ const AgentInfoView: React.FC<AgentInfoViewProps> = ({
<Descriptions.Item label="Updated At">{formatDate(agent.updated_at)}</Descriptions.Item>
</Descriptions>
<AgentCostView agent={agent} />
{agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
<div style={{ marginTop: 24 }}>
<Title>Skills</Title>
@@ -1,8 +1,17 @@
import React from "react";
import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Button, Icon } from "@tremor/react";
import { TrashIcon } from "@heroicons/react/outline";
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Button } from "@tremor/react";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, TrashIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { Agent } from "./types";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
interface AgentTableProps {
agentsList: Agent[];
@@ -23,71 +32,184 @@ const AgentTable: React.FC<AgentTableProps> = ({
isAdmin,
onAgentClick,
}) => {
if (isLoading) {
return <div>Loading agents...</div>;
}
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
if (!agentsList || agentsList.length === 0) {
return <div>No agents found. Create one to get started.</div>;
}
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
return (
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Agent Name</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
<TableHeaderCell>Created At</TableHeaderCell>
{isAdmin && <TableHeaderCell>Actions</TableHeaderCell>}
</TableRow>
</TableHead>
<TableBody>
{agentsList.map((agent) => (
<TableRow key={agent.agent_id}>
<TableCell>
<Tooltip title={agent.agent_name || ""}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => onAgentClick(agent.agent_id)}
>
{agent.agent_name || ""}
</Button>
</Tooltip>
</TableCell>
<TableCell>
{agent.agent_card_params?.description || "No description"}
</TableCell>
<TableCell>
{agent.created_at
? new Date(agent.created_at).toLocaleDateString()
: "N/A"}
</TableCell>
{isAdmin && (
<TableCell>
<div className="flex space-x-2">
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const columns: ColumnDef<Agent>[] = [
{
header: "Agent Name",
accessorKey: "agent_name",
cell: ({ row }) => {
const agent = row.original;
const name = agent.agent_name || "";
return (
<div className="flex items-center gap-2">
<Tooltip title={name}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[200px] justify-start"
onClick={() => onAgentClick(agent.agent_id)}
>
{name}
</Button>
</Tooltip>
<Tooltip title="Copy Agent ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(agent.agent_id);
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
);
},
},
{
header: "Description",
accessorKey: "agent_card_params.description",
cell: ({ row }) => {
const description = row.original.agent_card_params?.description || "No description";
return (
<span className="text-xs text-gray-600 block max-w-[300px] truncate">
{description}
</span>
);
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const agent = row.original;
return (
<Tooltip title={agent.created_at}>
<span className="text-xs">{formatDate(agent.created_at)}</span>
</Tooltip>
);
},
},
...(isAdmin
? [
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
const agent = row.original;
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete agent">
<Icon
icon={TrashIcon}
size="sm"
className="cursor-pointer text-red-500 hover:text-red-700"
<Button
size="xs"
variant="light"
color="red"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(agent.agent_id, agent.agent_name);
}}
aria-label="Delete agent"
icon={TrashIcon}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
/>
</Tooltip>
</div>
</TableCell>
);
},
},
]
: []),
];
const table = useReactTable({
data: agentsList,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
return (
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className="py-1 h-8"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
</div>
</TableCell>
</TableRow>
) : agentsList && agentsList.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No agents found. Create one to get started.</p>
</div>
</TableCell>
</TableRow>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableBody>
</Table>
</div>
</div>
);
};
export default AgentTable;
@@ -0,0 +1,23 @@
import React from "react";
import { Form, Input } from "antd";
import { AGENT_FORM_CONFIG } from "./agent_config";
const CostConfigFields: React.FC = () => {
return (
<>
{AGENT_FORM_CONFIG.cost.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
tooltip={field.tooltip}
>
<Input placeholder={field.placeholder} type="number" step="0.000001" />
</Form.Item>
))}
</>
);
};
export default CostConfigFields;
@@ -1,6 +1,10 @@
import React from "react";
import { Form, Input, Select } from "antd";
import { Form, Input, Select, Collapse } from "antd";
import { AgentCreateInfo, AgentCredentialFieldMetadata } from "../networking";
import { AGENT_FORM_CONFIG } from "./agent_config";
import CostConfigFields from "./cost_config_fields";
const { Panel } = Collapse;
interface DynamicAgentFormFieldsProps {
agentTypeInfo: AgentCreateInfo;
@@ -59,6 +63,12 @@ const DynamicAgentFormFields: React.FC<DynamicAgentFormFieldsProps> = ({
)}
</Form.Item>
))}
<Collapse style={{ marginBottom: 16 }}>
<Panel header={AGENT_FORM_CONFIG.cost.title} key={AGENT_FORM_CONFIG.cost.key}>
<CostConfigFields />
</Panel>
</Collapse>
</>
);
};
@@ -84,6 +94,17 @@ export const buildDynamicAgentData = (
}
}
// Add cost configuration
if (values.cost_per_query) {
litellmParams.cost_per_query = parseFloat(values.cost_per_query);
}
if (values.input_cost_per_token) {
litellmParams.input_cost_per_token = parseFloat(values.input_cost_per_token);
}
if (values.output_cost_per_token) {
litellmParams.output_cost_per_token = parseFloat(values.output_cost_per_token);
}
// Apply model_template if defined (e.g., "bedrock/agentcore/{agent_runtime_arn}")
if (agentTypeInfo.model_template) {
let model = agentTypeInfo.model_template;