[Feat] UI - Agent Gateway - set allowed agents by key, team (#17511)

* init schema.prisma

* init LiteLLM_ObjectPermissionTable with agents and agent_access_groups

* TestAgentRequestHandler

* refatctor agent list

* add AgentRequestHandler

* fix agent access controls by key/team

* feat - new migration for LiteLLM_AgentsTable

* fix add LiteLLM_ObjectPermissionBase with agent and agent groups

* add agent routes to llm api routes

* add agent routes as llm route

* add AgentPermissionsProps

* add agents on team/key create

* add agent selector on team/key

* add agent selector on key edit /info

* add AgentPermissions

* docs list + invoke agents
This commit is contained in:
Ishaan Jaff 2025-12-04 16:31:17 -08:00 • committed by GitHub
parent 8f592088ed
commit 575e769bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 492 additions and 14 deletions

View file

@ -33,10 +33,12 @@ The URL should be the invocation URL for your A2A agent (e.g., `http://localhost
## Invoking your Agents
Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM:
Use the [A2A Python SDK](https://pypi.org/project/a2a/) to invoke agents through LiteLLM.
- `base_url`: Your LiteLLM proxy URL + `/a2a/{agent_name}`
- `headers`: Include your LiteLLM Virtual Key for authentication
This example shows how to:
1. **List available agents** - Query `/v1/agents` to see which agents your key can access
2. **Select an agent** - Pick an agent from the list
3. **Invoke via A2A** - Use the A2A protocol to send messages to the agent
```python showLineNumbers title="invoke_a2a_agent.py"
from uuid import uuid4
@ -48,20 +50,36 @@ from a2a.types import MessageSendParams, SendMessageRequest
# === CONFIGURE THESE ===
LITELLM_BASE_URL = "http://localhost:4000" # Your LiteLLM proxy URL
LITELLM_VIRTUAL_KEY = "sk-1234" # Your LiteLLM Virtual Key
LITELLM_AGENT_NAME = "ij-local" # Agent name registered in LiteLLM
# =======================
async def main():
base_url = f"{LITELLM_BASE_URL}/a2a/{LITELLM_AGENT_NAME}"
headers = {"Authorization": f"Bearer {LITELLM_VIRTUAL_KEY}"}
async with httpx.AsyncClient(headers=headers) as httpx_client:
# Resolve agent card and create client
resolver = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
async with httpx.AsyncClient(headers=headers) as client:
# Step 1: List available agents
response = await client.get(f"{LITELLM_BASE_URL}/v1/agents")
agents = response.json()
print("Available agents:")
for agent in agents:
print(f" - {agent['agent_name']} (ID: {agent['agent_id']})")
if not agents:
print("No agents available for this key")
return
# Step 2: Select an agent and invoke it
selected_agent = agents[0]
agent_id = selected_agent["agent_id"]
agent_name = selected_agent["agent_name"]
print(f"\nInvoking: {agent_name}")
# Step 3: Use A2A protocol to invoke the agent
base_url = f"{LITELLM_BASE_URL}/a2a/{agent_id}"
resolver = A2ACardResolver(httpx_client=client, base_url=base_url)
agent_card = await resolver.get_agent_card()
client = A2AClient(httpx_client=httpx_client, agent_card=agent_card)
# Send a message
a2a_client = A2AClient(httpx_client=client, agent_card=agent_card)
request = SendMessageRequest(
id=str(uuid4()),
params=MessageSendParams(
@ -72,8 +90,8 @@ async def main():
}
),
)
response = await client.send_message(request)
print(response.model_dump(mode="json", exclude_none=True))
response = await a2a_client.send_message(request)
print(f"Response: {response.model_dump(mode='json', exclude_none=True, indent=4)}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -9,6 +9,7 @@ import {
import NumericalInput from "@/components/shared/numerical_input";
import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
import AgentSelector from "@/components/agent_management/AgentSelector";
import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings";
import ModelAliasManager from "@/components/common_components/ModelAliasManager";
import React, { useEffect, useState } from "react";
@ -210,6 +211,21 @@ const CreateTeamModal = ({
formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions;
delete formValues.mcp_tool_permissions;
}
// Handle agent permissions
if (formValues.allowed_agents_and_groups) {
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
if (!formValues.object_permission) {
formValues.object_permission = {};
}
if (agents && agents.length > 0) {
formValues.object_permission.agents = agents;
}
if (accessGroups && accessGroups.length > 0) {
formValues.object_permission.agent_access_groups = accessGroups;
}
delete formValues.allowed_agents_and_groups;
}
}
// Transform allowed_mcp_access_groups into object_permission
@ -546,6 +562,34 @@ const CreateTeamModal = ({
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Agent Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
label={
<span>
Allowed Agents{" "}
<Tooltip title="Select which agents or access groups this team can access">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_agents_and_groups"
className="mt-4"
help="Select agents or access groups this team can access"
>
<AgentSelector
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
value={form.getFieldValue("allowed_agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Logging Settings</b>

View file

@ -44,6 +44,7 @@ import {
import type { KeyResponse, Team } from "./key_team_helpers/key_list";
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "./mcp_server_management/MCPToolPermissions";
import AgentSelector from "./agent_management/AgentSelector";
import NotificationsManager from "./molecules/notifications_manager";
import { Organization, fetchMCPAccessGroups, getGuardrailsList, teamDeleteCall } from "./networking";
import NumericalInput from "./shared/numerical_input";
@ -448,6 +449,21 @@ const Teams: React.FC<TeamProps> = ({
delete formValues.allowed_mcp_access_groups;
}
// Handle agent permissions
if (formValues.allowed_agents_and_groups) {
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
if (!formValues.object_permission) {
formValues.object_permission = {};
}
if (agents && agents.length > 0) {
formValues.object_permission.agents = agents;
}
if (accessGroups && accessGroups.length > 0) {
formValues.object_permission.agent_access_groups = accessGroups;
}
delete formValues.allowed_agents_and_groups;
}
// Add model_aliases if any are defined
if (Object.keys(modelAliases).length > 0) {
formValues.model_aliases = modelAliases;
@ -1370,6 +1386,34 @@ const Teams: React.FC<TeamProps> = ({
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Agent Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
label={
<span>
Allowed Agents{" "}
<Tooltip title="Select which agents or access groups this team can access">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_agents_and_groups"
className="mt-4"
help="Select agents or access groups this team can access"
>
<AgentSelector
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
value={form.getFieldValue("allowed_agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Logging Settings</b>

View file

@ -0,0 +1,147 @@
import React, { useEffect, useState } from "react";
import { Select } from "antd";
import { getAgentsList } from "../networking";
interface Agent {
agent_id: string;
agent_name: string;
agent_config?: Record<string, any>;
agent_card_params?: Record<string, any>;
}
interface AgentSelectorProps {
onChange: (selected: {
agents: string[];
accessGroups: string[];
}) => void;
value?: {
agents: string[];
accessGroups: string[];
};
className?: string;
accessToken: string;
placeholder?: string;
disabled?: boolean;
}
const AgentSelector: React.FC<AgentSelectorProps> = ({
onChange,
value,
className,
accessToken,
placeholder = "Select agents",
disabled = false,
}) => {
const [agents, setAgents] = useState<Agent[]>([]);
const [accessGroups, setAccessGroups] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
if (!accessToken) return;
setLoading(true);
try {
const response = await getAgentsList(accessToken);
let agentsList = response?.agents || [];
setAgents(agentsList);
// Extract unique access groups from agents
const groups = new Set<string>();
agentsList.forEach((agent: Agent) => {
const agentAccessGroups = (agent as any).agent_access_groups;
if (agentAccessGroups && Array.isArray(agentAccessGroups)) {
agentAccessGroups.forEach((g: string) => groups.add(g));
}
});
setAccessGroups(Array.from(groups));
} catch (error) {
console.error("Error fetching agents:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [accessToken]);
// Combine options, access groups first
const options = [
...accessGroups.map((group) => ({
label: group,
value: `group:${group}`,
isAccessGroup: true,
searchText: `${group} Access Group`,
})),
...agents.map((agent) => ({
label: `${agent.agent_name || agent.agent_id}`,
value: agent.agent_id,
isAccessGroup: false,
searchText: `${agent.agent_name || agent.agent_id} ${agent.agent_id} Agent`,
})),
];
// Flatten value for Select
const selectedValues = [
...(value?.agents || []),
...(value?.accessGroups || []).map((g) => `group:${g}`),
];
// Handle selection
const handleChange = (selected: string[]) => {
const agentsSelected = selected.filter((v) => !v.startsWith("group:"));
const accessGroupsSelected = selected
.filter((v) => v.startsWith("group:"))
.map((v) => v.replace("group:", ""));
onChange({ agents: agentsSelected, accessGroups: accessGroupsSelected });
};
return (
<div>
<Select
mode="multiple"
placeholder={placeholder}
onChange={handleChange}
value={selectedValues}
loading={loading}
className={className}
showSearch
style={{ width: "100%" }}
disabled={disabled}
filterOption={(input, option) => {
const searchText = options.find((opt) => opt.value === option?.value)?.searchText || "";
return searchText.toLowerCase().includes(input.toLowerCase());
}}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} label={opt.label}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: opt.isAccessGroup ? "#52c41a" : "#722ed1",
flexShrink: 0,
}}
/>
<span style={{ flex: 1 }}>{opt.label}</span>
<span
style={{
color: opt.isAccessGroup ? "#52c41a" : "#722ed1",
fontSize: "12px",
fontWeight: 500,
opacity: 0.8,
}}
>
{opt.isAccessGroup ? "Access Group" : "Agent"}
</span>
</div>
</Select.Option>
))}
</Select>
</div>
);
};
export default AgentSelector;

View file

@ -83,6 +83,8 @@ export interface KeyResponse {
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
vector_stores: string[];
agents?: string[];
agent_access_groups?: string[];
};
auto_rotate?: boolean;
rotation_interval?: string;

View file

@ -2,6 +2,7 @@ import React from "react";
import { Text } from "@tremor/react";
import VectorStorePermissions from "./permissions/VectorStorePermissions";
import MCPServerPermissions from "./permissions/MCPServerPermissions";
import AgentPermissions from "./permissions/AgentPermissions";
interface ObjectPermission {
object_permission_id: string;
@ -9,6 +10,8 @@ interface ObjectPermission {
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
vector_stores: string[];
agents?: string[];
agent_access_groups?: string[];
}
interface ObjectPermissionsViewProps {
@ -28,9 +31,11 @@ export function ObjectPermissionsView({
const mcpServers = objectPermission?.mcp_servers || [];
const mcpAccessGroups = objectPermission?.mcp_access_groups || [];
const mcpToolPermissions = objectPermission?.mcp_tool_permissions || {};
const agents = objectPermission?.agents || [];
const agentAccessGroups = objectPermission?.agent_access_groups || [];
const content = (
<div className={variant === "card" ? "grid grid-cols-1 md:grid-cols-2 gap-6" : "space-y-4"}>
<div className={variant === "card" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" : "space-y-4"}>
<VectorStorePermissions vectorStores={vectorStores} accessToken={accessToken} />
<MCPServerPermissions
mcpServers={mcpServers}
@ -38,6 +43,11 @@ export function ObjectPermissionsView({
mcpToolPermissions={mcpToolPermissions}
accessToken={accessToken}
/>
<AgentPermissions
agents={agents}
agentAccessGroups={agentAccessGroups}
accessToken={accessToken}
/>
</div>
);

View file

@ -33,6 +33,7 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapDisplayToInternalNames } from "../callback_info_helpers";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import AgentSelector from "../agent_management/AgentSelector";
import ModelAliasManager from "../common_components/ModelAliasManager";
import NotificationsManager from "../molecules/notifications_manager";
import KeyLifecycleSettings from "../common_components/KeyLifecycleSettings";
@ -385,6 +386,26 @@ const CreateKey: React.FC<CreateKeyProps> = ({
delete formValues.allowed_mcp_access_groups;
}
// Transform allowed_agents_and_groups into object_permission format
if (
formValues.allowed_agents_and_groups &&
(formValues.allowed_agents_and_groups.agents?.length > 0 ||
formValues.allowed_agents_and_groups.accessGroups?.length > 0)
) {
if (!formValues.object_permission) {
formValues.object_permission = {};
}
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
if (agents && agents.length > 0) {
formValues.object_permission.agents = agents;
}
if (accessGroups && accessGroups.length > 0) {
formValues.object_permission.agent_access_groups = accessGroups;
}
// Remove the original field as it's now part of object_permission
delete formValues.allowed_agents_and_groups;
}
// Add model_aliases if any are defined
if (Object.keys(modelAliases).length > 0) {
formValues.aliases = JSON.stringify(modelAliases);
@ -1091,6 +1112,33 @@ const CreateKey: React.FC<CreateKeyProps> = ({
</AccordionBody>
</Accordion>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<b>Agent Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
label={
<span>
Allowed Agents{" "}
<Tooltip title="Select which agents or access groups this key can access">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_agents_and_groups"
help="Select agents or access groups this key can access"
>
<AgentSelector
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
value={form.getFieldValue("allowed_agents_and_groups")}
accessToken={accessToken}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
</AccordionBody>
</Accordion>
{premiumUser ? (
<Accordion className="mt-4 mb-4">
<AccordionHeader>

View file

@ -0,0 +1,111 @@
import React, { useState, useEffect } from "react";
import { Text, Badge } from "@tremor/react";
import { UserGroupIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { getAgentsList } from "../networking";
interface Agent {
agent_id: string;
agent_name: string;
agent_config?: Record<string, any>;
agent_card_params?: Record<string, any>;
}
interface AgentPermissionsProps {
agents: string[];
agentAccessGroups?: string[];
accessToken?: string | null;
}
export function AgentPermissions({
agents,
agentAccessGroups = [],
accessToken
}: AgentPermissionsProps) {
const [agentDetails, setAgentDetails] = useState<Agent[]>([]);
// Fetch agent details when component mounts
useEffect(() => {
const fetchAgentDetails = async () => {
if (accessToken && agents.length > 0) {
try {
const response = await getAgentsList(accessToken);
if (response && response.agents && Array.isArray(response.agents)) {
setAgentDetails(response.agents);
}
} catch (error) {
console.error("Error fetching agents:", error);
}
}
};
fetchAgentDetails();
}, [accessToken, agents.length]);
// Function to get display name for agent
const getAgentDisplayName = (agentId: string) => {
const agentDetail = agentDetails.find((agent) => agent.agent_id === agentId);
if (agentDetail) {
const truncatedId = agentId.length > 7 ? `${agentId.slice(0, 3)}...${agentId.slice(-4)}` : agentId;
return `${agentDetail.agent_name} (${truncatedId})`;
}
return agentId;
};
// Merge agents and access groups into one list
const mergedItems = [
...agents.map((agent) => ({ type: "agent", value: agent })),
...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
];
const totalCount = mergedItems.length;
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<UserGroupIcon className="h-4 w-4 text-purple-600" />
<Text className="font-semibold text-gray-900">Agents</Text>
<Badge color="purple" size="xs">
{totalCount}
</Badge>
</div>
{totalCount > 0 ? (
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-1">
{mergedItems.map((item, index) => (
<div key={index} className="space-y-2">
<div
className="flex items-center gap-3 py-2 px-3 rounded-lg border border-gray-200 bg-white"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{item.type === "agent" ? (
<Tooltip title={`Full ID: ${item.value}`} placement="top">
<div className="inline-flex items-center gap-2 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0"></span>
<span className="text-sm font-medium text-gray-900 truncate">{getAgentDisplayName(item.value)}</span>
</div>
</Tooltip>
) : (
<div className="inline-flex items-center gap-2 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></span>
<span className="text-sm font-medium text-gray-900 truncate">{item.value}</span>
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-green-600 bg-green-50 border border-green-200 rounded uppercase tracking-wide flex-shrink-0">
Group
</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
) : (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
<UserGroupIcon className="h-4 w-4 text-gray-400" />
<Text className="text-gray-500 text-sm">No agents or access groups configured</Text>
</div>
)}
</div>
);
}
export default AgentPermissions;

View file

@ -38,6 +38,7 @@ import { getModelDisplayName, unfurlWildcardModelsInList } from "../key_team_hel
import LoggingSettingsView from "../logging_settings_view";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import AgentSelector from "../agent_management/AgentSelector";
import NotificationsManager from "../molecules/notifications_manager";
import { fetchMCPAccessGroups } from "../networking";
import ObjectPermissionsView from "../object_permissions_view";
@ -95,6 +96,8 @@ export interface TeamData {
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
vector_stores: string[];
agents?: string[];
agent_access_groups?: string[];
};
team_member_budget_table: {
max_budget: number;
@ -433,6 +436,19 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
delete values.mcp_servers_and_groups;
delete values.mcp_tool_permissions;
// Handle agent permissions
const { agents, accessGroups: agentAccessGroups } = values.agents_and_groups || {
agents: [],
accessGroups: [],
};
if (agents && agents.length > 0) {
updateData.object_permission.agents = agents;
}
if (agentAccessGroups && agentAccessGroups.length > 0) {
updateData.object_permission.agent_access_groups = agentAccessGroups;
}
delete values.agents_and_groups;
const response = await teamUpdateCall(accessToken, updateData);
NotificationsManager.success("Team settings updated successfully");
@ -630,6 +646,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
accessGroups: info.object_permission?.mcp_access_groups || [],
},
mcp_tool_permissions: info.object_permission?.mcp_tool_permissions || {},
agents_and_groups: {
agents: info.object_permission?.agents || [],
accessGroups: info.object_permission?.agent_access_groups || [],
},
}}
layout="vertical"
>
@ -834,6 +854,15 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
)}
</Form.Item>
<Form.Item label="Agents / Access Groups" name="agents_and_groups">
<AgentSelector
onChange={(val) => form.setFieldValue("agents_and_groups", val)}
value={form.getFieldValue("agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
<Form.Item label="Organization ID" name="organization_id">
<Input type="" disabled />
</Form.Item>

View file

@ -10,6 +10,7 @@ import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata
import { KeyResponse } from "../key_team_helpers/key_list";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import AgentSelector from "../agent_management/AgentSelector";
import NotificationsManager from "../molecules/notifications_manager";
import { fetchMCPAccessGroups, getPromptsList, modelAvailableCall, tagListCall } from "../networking";
import { fetchTeamModels } from "../organisms/create_key_button";
@ -175,6 +176,10 @@ export function KeyEditView({
accessGroups: keyData.object_permission?.mcp_access_groups || [],
},
mcp_tool_permissions: keyData.object_permission?.mcp_tool_permissions || {},
agents_and_groups: {
agents: keyData.object_permission?.agents || [],
accessGroups: keyData.object_permission?.agent_access_groups || [],
},
logging_settings: extractLoggingSettings(keyData.metadata),
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
@ -511,6 +516,15 @@ export function KeyEditView({
)}
</Form.Item>
<Form.Item label="Agents / Access Groups" name="agents_and_groups">
<AgentSelector
onChange={(val) => form.setFieldValue("agents_and_groups", val)}
value={form.getFieldValue("agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
<Form.Item label="Team ID" name="team_id">
<Select placeholder="Select team" style={{ width: "100%" }}>
{/* Only show All Team Models if team has models */}

View file

@ -144,6 +144,17 @@ export default function KeyInfoView({
delete formValues.mcp_tool_permissions;
}
// Handle agent permissions
if (formValues.agents_and_groups !== undefined) {
const { agents, accessGroups } = formValues.agents_and_groups || { agents: [], accessGroups: [] };
formValues.object_permission = {
...formValues.object_permission,
agents: agents || [],
agent_access_groups: accessGroups || [],
};
delete formValues.agents_and_groups;
}
formValues.max_budget = mapEmptyStringToNull(formValues.max_budget);
formValues.tpm_limit = mapEmptyStringToNull(formValues.tpm_limit);
formValues.rpm_limit = mapEmptyStringToNull(formValues.rpm_limit);