mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-13 15:07:47 +00:00
[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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user