From bede40a90dd7aa1d73906bdceb2d71cb6324cf79 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 12 Dec 2025 16:37:31 -0800 Subject: [PATCH] [A2a gateway] Add agent cost tracking on UI (#17899) * add CostConfigFields * add CostConfigFields * add output_cost_per_token * refactor table * add agent cost view --- .../src/components/agents/agent_config.ts | 47 +++- .../src/components/agents/agent_cost_view.tsx | 46 ++++ .../components/agents/agent_form_fields.tsx | 7 + .../src/components/agents/agent_info.tsx | 3 + .../src/components/agents/agent_table.tsx | 234 +++++++++++++----- .../components/agents/cost_config_fields.tsx | 23 ++ .../agents/dynamic_agent_form_fields.tsx | 23 +- 7 files changed, 320 insertions(+), 63 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/agents/agent_cost_view.tsx create mode 100644 ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx diff --git a/ui/litellm-dashboard/src/components/agents/agent_config.ts b/ui/litellm-dashboard/src/components/agents/agent_config.ts index e930c2e07d..9dd41eed8a 100644 --- a/ui/litellm-dashboard/src/components/agents/agent_config.ts +++ b/ui/litellm-dashboard/src/components/agents/agent_config.ts @@ -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 = {}; + + 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, }; }; diff --git a/ui/litellm-dashboard/src/components/agents/agent_cost_view.tsx b/ui/litellm-dashboard/src/components/agents/agent_cost_view.tsx new file mode 100644 index 0000000000..38851486dc --- /dev/null +++ b/ui/litellm-dashboard/src/components/agents/agent_cost_view.tsx @@ -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 = ({ 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 ( +
+ Cost Configuration + + {params.cost_per_query !== undefined && ( + + ${params.cost_per_query} + + )} + {params.input_cost_per_token !== undefined && ( + + ${params.input_cost_per_token} + + )} + {params.output_cost_per_token !== undefined && ( + + ${params.output_cost_per_token} + + )} + +
+ ); +}; + +export default AgentCostView; + diff --git a/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx b/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx index 6d0c0820a4..4dc4ad6829 100644 --- a/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx +++ b/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx @@ -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 = ({ showAgentName = true ))} + {/* Cost Configuration */} + + + + {/* LiteLLM Parameters */} {AGENT_FORM_CONFIG.litellm.fields.map((field) => ( diff --git a/ui/litellm-dashboard/src/components/agents/agent_info.tsx b/ui/litellm-dashboard/src/components/agents/agent_info.tsx index c997ebc1e5..626e352bd3 100644 --- a/ui/litellm-dashboard/src/components/agents/agent_info.tsx +++ b/ui/litellm-dashboard/src/components/agents/agent_info.tsx @@ -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 = ({ {formatDate(agent.updated_at)} + + {agent.agent_card_params?.skills && agent.agent_card_params.skills.length > 0 && (
Skills diff --git a/ui/litellm-dashboard/src/components/agents/agent_table.tsx b/ui/litellm-dashboard/src/components/agents/agent_table.tsx index fed79d5abb..b141122c94 100644 --- a/ui/litellm-dashboard/src/components/agents/agent_table.tsx +++ b/ui/litellm-dashboard/src/components/agents/agent_table.tsx @@ -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 = ({ isAdmin, onAgentClick, }) => { - if (isLoading) { - return
Loading agents...
; - } + const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]); - if (!agentsList || agentsList.length === 0) { - return
No agents found. Create one to get started.
; - } + const formatDate = (dateString?: string) => { + if (!dateString) return "-"; + const date = new Date(dateString); + return date.toLocaleString(); + }; - return ( - - - - Agent Name - Description - Created At - {isAdmin && Actions} - - - - {agentsList.map((agent) => ( - - - - - - - - {agent.agent_card_params?.description || "No description"} - - - {agent.created_at - ? new Date(agent.created_at).toLocaleDateString() - : "N/A"} - - {isAdmin && ( - -
+ const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const columns: ColumnDef[] = [ + { + header: "Agent Name", + accessorKey: "agent_name", + cell: ({ row }) => { + const agent = row.original; + const name = agent.agent_name || ""; + return ( +
+ + + + + { + e.stopPropagation(); + copyToClipboard(agent.agent_id); + }} + className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" + /> + +
+ ); + }, + }, + { + header: "Description", + accessorKey: "agent_card_params.description", + cell: ({ row }) => { + const description = row.original.agent_card_params?.description || "No description"; + return ( + + {description} + + ); + }, + }, + { + header: "Created At", + accessorKey: "created_at", + cell: ({ row }) => { + const agent = row.original; + return ( + + {formatDate(agent.created_at)} + + ); + }, + }, + ...(isAdmin + ? [ + { + header: "Actions", + id: "actions", + enableSorting: false, + cell: ({ row }: any) => { + const agent = row.original; + + return ( +
- { 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" />
- + ); + }, + }, + ] + : []), + ]; + + const table = useReactTable({ + data: agentsList, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSorting: true, + }); + + return ( +
+
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+
+
+ ))} +
+ ))} +
+ + {isLoading ? ( + + +
+

Loading...

+
+
+
+ ) : agentsList && agentsList.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No agents found. Create one to get started.

+
+
+
)} - - ))} -
-
+ + +
+ ); }; export default AgentTable; - diff --git a/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx b/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx new file mode 100644 index 0000000000..b07aba8dc7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx @@ -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) => ( + + + + ))} + + ); +}; + +export default CostConfigFields; + diff --git a/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx b/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx index 67f0f470ab..55a4a62953 100644 --- a/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx +++ b/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx @@ -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 = ({ )} ))} + + + + + + ); }; @@ -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;