Provider Configurations
diff --git a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
index 321b37fc9a..9ad01fb082 100644
--- a/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
+++ b/ui/app/workspace/virtual-keys/views/virtualKeySheet.tsx
@@ -53,7 +53,19 @@ interface VirtualKeySheetProps {
const providerConfigSchema = z.object({
id: z.number().optional(),
provider: z.string().min(1, "Provider is required"),
- weight: z.union([z.number().min(0, "Weight must be at least 0").max(1, "Weight must be at most 1"), z.string()]),
+ weight: z
+ .union([
+ z.literal("").transform(() => undefined as undefined),
+ z
+ .string()
+ .transform((v) => {
+ const n = Number.parseFloat(v);
+ return Number.isNaN(n) ? undefined : n;
+ })
+ .pipe(z.number().min(0, "Weight must be at least 0").max(1, "Weight must be at most 1").optional()),
+ z.number().min(0, "Weight must be at least 0").max(1, "Weight must be at most 1"),
+ ])
+ .optional(),
allowed_models: z.array(z.string()).optional(),
key_ids: z.array(z.string()).optional(), // Keys associated with this provider config
// Provider-level budget
@@ -156,7 +168,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
const availableProviders = providersData || [];
// Form setup
- const form = useForm
({
+ const form = useForm, unknown, FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: virtualKey?.name || "",
@@ -164,20 +176,21 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
providerConfigs:
virtualKey?.provider_configs?.map((config) => ({
...config,
+ weight: config.weight ?? "",
key_ids: config.keys?.map((key) => key.key_id) || [],
budget: config.budget
? {
- max_limit: String(config.budget.max_limit),
- reset_duration: config.budget.reset_duration,
- }
+ max_limit: String(config.budget.max_limit),
+ reset_duration: config.budget.reset_duration,
+ }
: undefined,
rate_limit: config.rate_limit
? {
- token_max_limit: config.rate_limit.token_max_limit ? String(config.rate_limit.token_max_limit) : undefined,
- token_reset_duration: config.rate_limit.token_reset_duration,
- request_max_limit: config.rate_limit.request_max_limit ? String(config.rate_limit.request_max_limit) : undefined,
- request_reset_duration: config.rate_limit.request_reset_duration,
- }
+ token_max_limit: config.rate_limit.token_max_limit ? String(config.rate_limit.token_max_limit) : undefined,
+ token_reset_duration: config.rate_limit.token_reset_duration,
+ request_max_limit: config.rate_limit.request_max_limit ? String(config.rate_limit.request_max_limit) : undefined,
+ request_reset_duration: config.rate_limit.request_reset_duration,
+ }
: undefined,
})) || [],
mcpConfigs:
@@ -260,7 +273,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
const newConfig = {
provider: provider,
- weight: 0.5, // Default weight, user can adjust
+ weight: "" as string | number, // Default empty string = excluded from weighted routing until user sets a weight
allowed_models: [],
key_ids: [],
};
@@ -335,23 +348,24 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
): any[] => {
return configs.map((config) => ({
...config,
- weight: typeof config.weight === "string" ? parseFloat(config.weight) || 0 : config.weight,
- budget: (() => {
- const budgetMaxLimit = normalizeNumericField(config.budget?.max_limit);
- if (budgetMaxLimit !== undefined) {
- return {
- max_limit: budgetMaxLimit,
- reset_duration: config.budget?.reset_duration || "1M",
- };
- }
-
- const existingConfig = existingConfigs?.find((item) => (config.id ? item.id === config.id : item.provider === config.provider));
- if (existingConfig?.budget) {
- return {};
- }
-
- return undefined;
- })(),
+ weight: config.weight === "" || config.weight === undefined || config.weight === null
+ ? null
+ : typeof config.weight === "string" ? (Number.isNaN(parseFloat(config.weight)) ? null : parseFloat(config.weight)) : config.weight, budget: (() => {
+ const budgetMaxLimit = normalizeNumericField(config.budget?.max_limit);
+ if (budgetMaxLimit !== undefined) {
+ return {
+ max_limit: budgetMaxLimit,
+ reset_duration: config.budget?.reset_duration || "1M",
+ };
+ }
+
+ const existingConfig = existingConfigs?.find((item) => (config.id ? item.id === config.id : item.provider === config.provider));
+ if (existingConfig?.budget) {
+ return {};
+ }
+
+ return undefined;
+ })(),
rate_limit: (() => {
const tokenMaxLimit = normalizeIntegerField(config.rate_limit?.token_max_limit);
const requestMaxLimit = normalizeIntegerField(config.rate_limit?.request_max_limit);
@@ -553,8 +567,8 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
- Configure which providers this virtual key can use and their specific settings. Leave empty to allow all
- providers.
+ Configure which providers this virtual key can use and their specific settings. Leave empty to block all
+ providers. Add providers to allow them.
@@ -643,7 +657,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
: ProviderLabels[config.provider as ProviderName]}
Weight
{
const inputValue = e.target.value;
// Allow empty string, numbers, and partial decimal inputs like "0."
@@ -683,6 +698,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
Allowed Models
type to search
{
const providerKeys = availableKeys.filter((key) => key.provider === config.provider);
@@ -894,8 +910,9 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
- Configure which MCP clients this virtual key can use and their allowed tools. Leave empty to allow all MCP
- clients and tools.
+ Configure which MCP clients this virtual key can use and their allowed tools. Leaving this section empty
+ blocks all MCP tools. After adding an MCP client, you must select specific tools or choose{" "}
+ Allow All Tools to grant tool access.
@@ -986,20 +1003,39 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
{config.mcp_client_name}
arr.findIndex((t) => t.name === tool.name) === index)
- .map((tool) => ({
- label: tool.name,
- value: tool.name,
- description: tool.description,
- }))}
+ options={[
+ {
+ label: "Allow All Tools",
+ value: "*",
+ description: "Allow all current and future tools (including dynamically fetched ones)",
+ },
+ ...[...availableTools, ...enabledToolsByConfig]
+ .filter((tool, index, arr) => arr.findIndex((t) => t.name === tool.name) === index)
+ .map((tool) => ({
+ label: tool.name,
+ value: tool.name,
+ description: tool.description,
+ })),
+ ]}
defaultValue={selectedTools}
- onValueChange={(tools: string[]) => handleUpdateMCPConfig(index, "tools_to_execute", tools)}
+ onValueChange={(tools: string[]) => {
+ const hadStar = selectedTools.includes("*");
+ const hasStar = tools.includes("*");
+ if (!hadStar && hasStar) {
+ // Just selected "Allow All Tools" — set to ["*"] only
+ handleUpdateMCPConfig(index, "tools_to_execute", ["*"]);
+ } else if (hadStar && hasStar && tools.length > 1) {
+ // Had "*", still has "*", but user also selected a specific tool — drop "*"
+ handleUpdateMCPConfig(index, "tools_to_execute", tools.filter((t) => t !== "*"));
+ } else {
+ handleUpdateMCPConfig(index, "tools_to_execute", tools);
+ }
+ }}
placeholder={
selectedTools.length === 0
? "No tools selected"
: selectedTools.includes("*")
- ? "All tools selected"
+ ? "All tools allowed"
: "Select tools..."
}
variant="inverted"
@@ -1010,7 +1046,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
/>
- handleRemoveMCPClient(index)}>
+ handleRemoveMCPClient(index)} data-testid={`vk-delete-mcp-${index}`}>
@@ -1172,7 +1208,7 @@ export default function VirtualKeySheet({ virtualKey, teams, customers, onSave,
-
+
No Assignment
{teams?.length > 0 && Assign to Team }
{customers?.length > 0 && Assign to Customer }
diff --git a/ui/components/ui/numberAndSelect.tsx b/ui/components/ui/numberAndSelect.tsx
index 5eeae048f1..a443be2331 100644
--- a/ui/components/ui/numberAndSelect.tsx
+++ b/ui/components/ui/numberAndSelect.tsx
@@ -12,6 +12,7 @@ const NumberAndSelect = ({
onChangeSelect,
options,
labelClassName,
+ placeholder = "100",
dataTestId,
}: {
id: string;
@@ -22,6 +23,7 @@ const NumberAndSelect = ({
onChangeSelect: (value: string) => void;
options: { label: string; value: string }[];
labelClassName?: string;
+ placeholder?: string;
dataTestId?: string;
}) => {
return (
@@ -33,7 +35,7 @@ const NumberAndSelect = ({
{
const inputValue = e.target.value;
diff --git a/ui/lib/types/governance.ts b/ui/lib/types/governance.ts
index d9293c341f..cb35cfef3f 100644
--- a/ui/lib/types/governance.ts
+++ b/ui/lib/types/governance.ts
@@ -87,7 +87,7 @@ export interface VirtualKey {
export interface VirtualKeyProviderConfig {
id?: number;
provider: string;
- weight: number;
+ weight: number | null;
allowed_models: string[];
budget?: Budget;
rate_limit?: RateLimit;
@@ -130,7 +130,7 @@ export interface UsageStats {
// Request interfaces for provider config operations
export interface VirtualKeyProviderConfigRequest {
provider: string;
- weight?: number;
+ weight?: number | null;
allowed_models?: string[];
budget?: CreateBudgetRequest;
rate_limit?: CreateRateLimitRequest;
@@ -140,7 +140,7 @@ export interface VirtualKeyProviderConfigRequest {
export interface VirtualKeyProviderConfigUpdateRequest {
id?: number;
provider: string;
- weight?: number;
+ weight?: number | null;
allowed_models?: string[];
budget?: UpdateBudgetRequest;
rate_limit?: UpdateRateLimitRequest;