From c98da10b842062fcba669e305bc84e5832a0e71c Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Thu, 5 Mar 2026 16:34:25 -0800 Subject: [PATCH 1/6] feat: Hashicorp Vault config UI components Add settings panel, edit modal, and delete flow for managing Hashicorp Vault configuration from the admin UI. Extract shared constants, use React Query hooks, and align error handling with deriveErrorMessage pattern. --- .../useDeleteHashicorpVaultConfig.ts | 21 ++ .../useHashicorpVaultConfig.ts | 23 ++ .../useUpdateHashicorpVaultConfig.ts | 21 ++ .../EditHashicorpVaultModal.tsx | 171 ++++++++++++ .../HashicorpVault/HashicorpVault.tsx | 250 ++++++++++++++++++ .../HashicorpVaultEmptyPlaceholder.tsx | 30 +++ .../AdminSettings/HashicorpVault/constants.ts | 20 ++ .../src/components/networking.tsx | 89 +++++++ 8 files changed, 625 insertions(+) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts new file mode 100644 index 00000000000..365ee7bf9a9 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts @@ -0,0 +1,21 @@ +import { deleteHashicorpVaultConfig } from "@/components/networking"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); + +export const useDeleteHashicorpVaultConfig = (accessToken: string | null) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return deleteHashicorpVaultConfig(accessToken); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: hashicorpVaultKeys.all }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts new file mode 100644 index 00000000000..62b6d8cfab4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts @@ -0,0 +1,23 @@ +import { getHashicorpVaultConfig } from "@/components/networking"; +import { useQuery } from "@tanstack/react-query"; +import useAuthorized from "../useAuthorized"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); + +export const useHashicorpVaultConfig = () => { + const { accessToken } = useAuthorized(); + + return useQuery>({ + queryKey: hashicorpVaultKeys.list({}), + queryFn: async () => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return getHashicorpVaultConfig(accessToken); + }, + enabled: !!accessToken, + staleTime: 60 * 60 * 1000, + gcTime: 60 * 60 * 1000, + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts new file mode 100644 index 00000000000..cea447c4908 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts @@ -0,0 +1,21 @@ +import { updateHashicorpVaultConfig } from "@/components/networking"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); + +export const useUpdateHashicorpVaultConfig = (accessToken: string | null) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (config: Record) => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return updateHashicorpVaultConfig(accessToken, config); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: hashicorpVaultKeys.all }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx new file mode 100644 index 00000000000..314c5456ab0 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig"; +import { useUpdateHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import NotificationManager from "@/components/molecules/notifications_manager"; +import { Button, Divider, Form, Input, Modal, Space, Typography } from "antd"; +import React, { useEffect } from "react"; +import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants"; + +interface FieldGroup { + title: string; + subtitle?: string; + fields: string[]; +} + +const FIELD_GROUPS: FieldGroup[] = [ + { + title: "Connection", + fields: ["vault_addr", "vault_namespace", "vault_mount_name", "vault_path_prefix"], + }, + { + title: "Token Authentication", + subtitle: "Use a Vault token to authenticate. Only one auth method is required.", + fields: ["vault_token"], + }, + { + title: "AppRole Authentication", + subtitle: "Use AppRole credentials to authenticate. Only one auth method is required.", + fields: ["approle_role_id", "approle_secret_id", "approle_mount_path"], + }, + { + title: "TLS", + subtitle: "Optional client certificate for mTLS.", + fields: ["client_cert", "client_key", "vault_cert_role"], + }, +]; + +interface EditHashicorpVaultModalProps { + isVisible: boolean; + onCancel: () => void; + onSuccess: () => void; +} + +const EditHashicorpVaultModal: React.FC = ({ + isVisible, + onCancel, + onSuccess, +}) => { + const [form] = Form.useForm(); + const { accessToken } = useAuthorized(); + const { data } = useHashicorpVaultConfig(); + const { mutateAsync, isPending } = useUpdateHashicorpVaultConfig(accessToken); + + const schema = data?.field_schema; + const properties = schema?.properties ?? {}; + const rawValues = data?.values ?? {}; + + useEffect(() => { + if (isVisible && data) { + form.resetFields(); + // Only set non-sensitive fields — sensitive ones show as placeholders + const formValues: Record = {}; + for (const [key, value] of Object.entries(rawValues)) { + if (!SENSITIVE_FIELDS.has(key)) { + formValues[key] = value; + } + } + form.setFieldsValue(formValues); + } + }, [isVisible, data, form]); + + const handleSubmit = async (formValues: Record) => { + const config: Record = {}; + for (const [key, value] of Object.entries(formValues)) { + if (value !== undefined && value !== null && value !== "") { + // Non-empty value → update + config[key] = value; + } else if (!SENSITIVE_FIELDS.has(key)) { + // Non-sensitive field cleared → send "" to clear it on the backend + config[key] = ""; + } + // Sensitive field left blank → omit from payload (keep existing) + } + + await mutateAsync(config, { + onSuccess: () => { + NotificationManager.success("Hashicorp Vault configuration updated successfully"); + onSuccess(); + }, + onError: (err) => { + NotificationManager.fromBackend(err); + }, + }); + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + const renderField = (fieldName: string) => { + const fieldSchema = properties[fieldName]; + if (!fieldSchema) return null; + + const rules = + fieldName === "vault_addr" + ? [{ pattern: /^https?:\/\/.+/, message: "Must start with http:// or https://" }] + : undefined; + + const isSensitive = SENSITIVE_FIELDS.has(fieldName); + const existingValue = rawValues[fieldName]; + const hasExistingValue = isSensitive && existingValue != null && existingValue !== ""; + const placeholder = hasExistingValue + ? `Leave blank to keep existing (${existingValue})` + : fieldSchema?.description; + + return ( + + {isSensitive ? ( + + ) : ( + + )} + + ); + }; + + return ( + + + + + } + onCancel={handleCancel} + > +
+ {FIELD_GROUPS.map((group, index) => ( +
+ {index > 0 && } + + {group.title} + + {group.subtitle && ( + + {group.subtitle} + + )} + {group.fields.map(renderField)} +
+ ))} +
+
+ ); +}; + +export default EditHashicorpVaultModal; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx new file mode 100644 index 00000000000..a2693903c52 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useState } from "react"; +import { useHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig"; +import { useDeleteHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig"; +import { useUpdateHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; +import NotificationManager from "@/components/molecules/notifications_manager"; +import { testHashicorpVaultConnection } from "@/components/networking"; +import { Alert, Button, Card, Descriptions, Skeleton, Space, Typography } from "antd"; +import { Edit, KeyRound, PlugZap, Trash2 } from "lucide-react"; +import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants"; +import EditHashicorpVaultModal from "./EditHashicorpVaultModal"; +import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder"; + +const { Title, Text } = Typography; + +function detectAuthMethod(values: Record): string { + if (values.vault_token) return "Token"; + if (values.approle_role_id || values.approle_secret_id) return "AppRole"; + return "None"; +} + +const descriptionsConfig = { + column: { xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, +}; + +export default function HashicorpVault() { + const { accessToken } = useAuthorized(); + const { data, isLoading, isError, error, refetch } = useHashicorpVaultConfig(); + const { mutate: deleteConfig, isPending: isDeleting } = useDeleteHashicorpVaultConfig(accessToken); + const { mutateAsync: updateConfig } = useUpdateHashicorpVaultConfig(accessToken); + + const [isEditModalVisible, setIsEditModalVisible] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [clearingField, setClearingField] = useState(null); + const [isClearingField, setIsClearingField] = useState(false); + const [isTesting, setIsTesting] = useState(false); + + const rawValues = data?.values ?? {}; + const isConfigured = Boolean(rawValues.vault_addr); + + const handleTestConnection = async () => { + if (!accessToken) return; + setIsTesting(true); + try { + const result = await testHashicorpVaultConnection(accessToken); + NotificationManager.success(result.message || "Connection to Vault successful!"); + } catch (err) { + NotificationManager.fromBackend(err); + } finally { + setIsTesting(false); + } + }; + + const handleDelete = () => { + deleteConfig(undefined, { + onSuccess: () => { + NotificationManager.success("Hashicorp Vault configuration deleted"); + setIsDeleteModalOpen(false); + }, + onError: (err) => { + NotificationManager.fromBackend(err); + }, + }); + }; + + const handleClearField = async () => { + if (!clearingField) return; + setIsClearingField(true); + try { + await updateConfig({ [clearingField]: "" }); + NotificationManager.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`); + setClearingField(null); + refetch(); + } catch (err) { + NotificationManager.fromBackend(err); + } finally { + setIsClearingField(false); + } + }; + + const renderValue = (key: string) => { + const value = rawValues[key]; + if (!value) { + return Not configured; + } + if (SENSITIVE_FIELDS.has(key)) { + return ( +
+ {value} +
+ ); + } + return {value}; + }; + + const renderSettings = () => { + // Only show fields that have values, plus auth method + const fieldsToShow = Object.entries(rawValues).filter( + ([_, value]) => value != null && value !== "" + ); + + if (fieldsToShow.length === 0) return null; + + return ( + + + {detectAuthMethod(rawValues)} + + {fieldsToShow.map(([key]) => ( + + {renderValue(key)} + + ))} + + ); + }; + + return ( + <> + {isLoading ? ( + + + + ) : isError ? ( + + + + ) : ( + + + + {/* Header */} +
+
+ +
+ Hashicorp Vault + Manage secret manager configuration +
+
+ +
+ {isConfigured && ( + <> + + + + + )} +
+
+ + {isConfigured && ( + + vault kv put secret/SECRET_NAME key=secret_value +
+ + View documentation + + + } + /> + )} + + {isConfigured ? ( + renderSettings() + ) : ( + setIsEditModalVisible(true)} /> + )} +
+
+
+ )} + + setIsEditModalVisible(false)} + onSuccess={() => { + setIsEditModalVisible(false); + refetch(); + }} + /> + + setIsDeleteModalOpen(false)} + onOk={handleDelete} + confirmLoading={isDeleting} + /> + + setClearingField(null)} + onOk={handleClearField} + confirmLoading={isClearingField} + /> + + ); +} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx new file mode 100644 index 00000000000..49860fc7617 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx @@ -0,0 +1,30 @@ +import { Empty, Typography, Button } from "antd"; + +const { Title, Paragraph } = Typography; + +interface HashicorpVaultEmptyPlaceholderProps { + onAdd: () => void; +} + +export default function HashicorpVaultEmptyPlaceholder({ onAdd }: HashicorpVaultEmptyPlaceholderProps) { + return ( +
+ + No Vault Configuration Found + + Configure Hashicorp Vault to securely manage provider API keys and secrets + for your LiteLLM deployment. + +
+ } + > + + + + ); +} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts new file mode 100644 index 00000000000..ef924f5f122 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts @@ -0,0 +1,20 @@ +export const SENSITIVE_FIELDS = new Set([ + "vault_token", + "approle_role_id", + "approle_secret_id", + "client_key", +]); + +export const FIELD_LABELS: Record = { + vault_addr: "Vault Address", + vault_namespace: "Namespace", + vault_mount_name: "KV Mount Name", + vault_path_prefix: "Path Prefix", + vault_token: "Token", + approle_role_id: "Role ID", + approle_secret_id: "Secret ID", + approle_mount_path: "Mount Path", + client_cert: "Client Certificate", + client_key: "Client Key", + vault_cert_role: "Certificate Role", +}; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 0df6d813d7c..a8f6013726c 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -9659,6 +9659,95 @@ export const updateUiSettings = async (accessToken: string, settings: Record { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const detail = errorData?.detail; + const errorMessage = + (typeof detail === "object" && detail?.error) || + (typeof detail === "string" && detail) || + deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const updateHashicorpVaultConfig = async ( + accessToken: string, + config: Record, +) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(config), + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const deleteHashicorpVaultConfig = async (accessToken: string) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "DELETE", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const testHashicorpVaultConnection = async (accessToken: string) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault/test_connection` + : `/config_overrides/hashicorp_vault/test_connection`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + // ============================================================ // Claude Code Marketplace Networking Functions // ============================================================ From 8388e77b592e99fc069f12af45e616c8f8e1f7d0 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Fri, 6 Mar 2026 18:26:30 -0800 Subject: [PATCH 2/6] wire in the hashicorp settings --- ui/litellm-dashboard/src/components/AdminPanel.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/litellm-dashboard/src/components/AdminPanel.tsx b/ui/litellm-dashboard/src/components/AdminPanel.tsx index 1d69606ff2b..7528b081ff3 100644 --- a/ui/litellm-dashboard/src/components/AdminPanel.tsx +++ b/ui/litellm-dashboard/src/components/AdminPanel.tsx @@ -23,6 +23,7 @@ import { addAllowedIP, deleteAllowedIP, getAllowedIPs, getSSOSettings } from "./ import SCIMConfig from "./SCIM"; import SSOSettings from "./Settings/AdminSettings/SSOSettings/SSOSettings"; import UISettings from "./Settings/AdminSettings/UISettings/UISettings"; +import HashicorpVault from "./Settings/AdminSettings/HashicorpVault/HashicorpVault"; import SSOModals from "./SSOModals"; import UIAccessControlForm from "./UIAccessControlForm"; @@ -361,6 +362,11 @@ const AdminPanel: React.FC = ({ proxySettings }) => { ), children: , }, + { + key: "hashicorp-vault", + label: "Hashicorp Vault", + children: , + }, ]; return ( From 37de206c5bf792d2dd44adbbd41f8ebe0a8212bb Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 7 Mar 2026 08:58:28 -0800 Subject: [PATCH 3/6] polish changes --- .../useDeleteHashicorpVaultConfig.ts | 4 +- .../useHashicorpVaultConfig.ts | 2 +- .../useUpdateHashicorpVaultConfig.ts | 4 +- .../EditHashicorpVaultModal.tsx | 6 +- .../HashicorpVault/HashicorpVault.tsx | 160 +++++++++--------- .../src/components/networking.tsx | 6 +- 6 files changed, 83 insertions(+), 99 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts index 365ee7bf9a9..c11969fb015 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts @@ -1,8 +1,6 @@ import { deleteHashicorpVaultConfig } from "@/components/networking"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { createQueryKeys } from "../common/queryKeysFactory"; - -const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); +import { hashicorpVaultKeys } from "./useHashicorpVaultConfig"; export const useDeleteHashicorpVaultConfig = (accessToken: string | null) => { const queryClient = useQueryClient(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts index 62b6d8cfab4..958dac2479c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import useAuthorized from "../useAuthorized"; import { createQueryKeys } from "../common/queryKeysFactory"; -const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); +export const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); export const useHashicorpVaultConfig = () => { const { accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts index cea447c4908..4eee6c29ad0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts @@ -1,8 +1,6 @@ import { updateHashicorpVaultConfig } from "@/components/networking"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { createQueryKeys } from "../common/queryKeysFactory"; - -const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig"); +import { hashicorpVaultKeys } from "./useHashicorpVaultConfig"; export const useUpdateHashicorpVaultConfig = (accessToken: string | null) => { const queryClient = useQueryClient(); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx index 314c5456ab0..58be384a667 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx @@ -50,7 +50,7 @@ const EditHashicorpVaultModal: React.FC = ({ const [form] = Form.useForm(); const { accessToken } = useAuthorized(); const { data } = useHashicorpVaultConfig(); - const { mutateAsync, isPending } = useUpdateHashicorpVaultConfig(accessToken); + const { mutate, isPending } = useUpdateHashicorpVaultConfig(accessToken); const schema = data?.field_schema; const properties = schema?.properties ?? {}; @@ -70,7 +70,7 @@ const EditHashicorpVaultModal: React.FC = ({ } }, [isVisible, data, form]); - const handleSubmit = async (formValues: Record) => { + const handleSubmit = (formValues: Record) => { const config: Record = {}; for (const [key, value] of Object.entries(formValues)) { if (value !== undefined && value !== null && value !== "") { @@ -83,7 +83,7 @@ const EditHashicorpVaultModal: React.FC = ({ // Sensitive field left blank → omit from payload (keep existing) } - await mutateAsync(config, { + mutate(config, { onSuccess: () => { NotificationManager.success("Hashicorp Vault configuration updated successfully"); onSuccess(); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx index a2693903c52..d019f6ee011 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx @@ -28,14 +28,13 @@ const descriptionsConfig = { export default function HashicorpVault() { const { accessToken } = useAuthorized(); - const { data, isLoading, isError, error, refetch } = useHashicorpVaultConfig(); + const { data, isLoading, isError, error } = useHashicorpVaultConfig(); const { mutate: deleteConfig, isPending: isDeleting } = useDeleteHashicorpVaultConfig(accessToken); - const { mutateAsync: updateConfig } = useUpdateHashicorpVaultConfig(accessToken); + const { mutate: updateConfig, isPending: isClearingField } = useUpdateHashicorpVaultConfig(accessToken); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [clearingField, setClearingField] = useState(null); - const [isClearingField, setIsClearingField] = useState(false); const [isTesting, setIsTesting] = useState(false); const rawValues = data?.values ?? {}; @@ -66,19 +65,17 @@ export default function HashicorpVault() { }); }; - const handleClearField = async () => { + const handleClearField = () => { if (!clearingField) return; - setIsClearingField(true); - try { - await updateConfig({ [clearingField]: "" }); - NotificationManager.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`); - setClearingField(null); - refetch(); - } catch (err) { - NotificationManager.fromBackend(err); - } finally { - setIsClearingField(false); - } + updateConfig({ [clearingField]: "" }, { + onSuccess: () => { + NotificationManager.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`); + setClearingField(null); + }, + onError: (err) => { + NotificationManager.fromBackend(err); + }, + }); }; const renderValue = (key: string) => { @@ -140,84 +137,79 @@ export default function HashicorpVault() { /> ) : ( - - - - {/* Header */} -
-
- -
- Hashicorp Vault - Manage secret manager configuration -
-
- -
- {isConfigured && ( - <> - - - - - )} + + + {/* Header */} +
+
+ +
+ Hashicorp Vault + Manage secret manager configuration
- {isConfigured && ( - - vault kv put secret/SECRET_NAME key=secret_value -
- - View documentation - - - } - /> - )} +
+ {isConfigured && ( + <> + + + + + )} +
+
- {isConfigured ? ( - renderSettings() - ) : ( - setIsEditModalVisible(true)} /> - )} -
-
- + {isConfigured && ( + + vault kv put secret/SECRET_NAME key=secret_value +
+ + View documentation + + + } + /> + )} + + {isConfigured ? ( + renderSettings() + ) : ( + setIsEditModalVisible(true)} /> + )} + + )} setIsEditModalVisible(false)} - onSuccess={() => { - setIsEditModalVisible(false); - refetch(); - }} + onSuccess={() => setIsEditModalVisible(false)} /> { }); if (!response.ok) { const errorData = await response.json(); - const detail = errorData?.detail; - const errorMessage = - (typeof detail === "object" && detail?.error) || - (typeof detail === "string" && detail) || - deriveErrorMessage(errorData); + const errorMessage = deriveErrorMessage(errorData); throw new Error(errorMessage); } const data = await response.json(); From 0dff602423362269d66b89ea7a02ad3a83d71e5f Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 7 Mar 2026 09:18:32 -0800 Subject: [PATCH 4/6] fix: align frontend sensitive fields with backend --- .../Settings/AdminSettings/HashicorpVault/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts index ef924f5f122..65f666fad8c 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/constants.ts @@ -1,6 +1,5 @@ export const SENSITIVE_FIELDS = new Set([ "vault_token", - "approle_role_id", "approle_secret_id", "client_key", ]); From 536457fb31b272087b63c8ac10d91b4ff5e32ff4 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 7 Mar 2026 10:30:40 -0800 Subject: [PATCH 5/6] address review: use AntD Flex/Space, extract vault API from networking --- .../configOverrides/hashicorpVaultApi.ts | 86 +++++++++++++++++++ .../useDeleteHashicorpVaultConfig.ts | 2 +- .../useHashicorpVaultConfig.ts | 2 +- .../useUpdateHashicorpVaultConfig.ts | 2 +- .../HashicorpVault/HashicorpVault.tsx | 20 ++--- .../src/components/networking.tsx | 84 ------------------ 6 files changed, 99 insertions(+), 97 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi.ts new file mode 100644 index 00000000000..edf18860ec1 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi.ts @@ -0,0 +1,86 @@ +import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage } from "@/components/networking"; + +export const getHashicorpVaultConfig = async (accessToken: string) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "GET", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const updateHashicorpVaultConfig = async ( + accessToken: string, + config: Record, +) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "POST", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(config), + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const deleteHashicorpVaultConfig = async (accessToken: string) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` + : `/config_overrides/hashicorp_vault`; + const response = await fetch(url, { + method: "DELETE", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; + +export const testHashicorpVaultConnection = async (accessToken: string) => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/config_overrides/hashicorp_vault/test_connection` + : `/config_overrides/hashicorp_vault/test_connection`; + const response = await fetch(url, { + method: "POST", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + throw new Error(errorMessage); + } + const data = await response.json(); + return data; +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts index c11969fb015..a7d4d06b55c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig.ts @@ -1,4 +1,4 @@ -import { deleteHashicorpVaultConfig } from "@/components/networking"; +import { deleteHashicorpVaultConfig } from "./hashicorpVaultApi"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { hashicorpVaultKeys } from "./useHashicorpVaultConfig"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts index 958dac2479c..83d5909d0ac 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig.ts @@ -1,4 +1,4 @@ -import { getHashicorpVaultConfig } from "@/components/networking"; +import { getHashicorpVaultConfig } from "./hashicorpVaultApi"; import { useQuery } from "@tanstack/react-query"; import useAuthorized from "../useAuthorized"; import { createQueryKeys } from "../common/queryKeysFactory"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts index 4eee6c29ad0..2317912a68b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig.ts @@ -1,4 +1,4 @@ -import { updateHashicorpVaultConfig } from "@/components/networking"; +import { updateHashicorpVaultConfig } from "./hashicorpVaultApi"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { hashicorpVaultKeys } from "./useHashicorpVaultConfig"; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx index d019f6ee011..b923fafeb1e 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx @@ -7,8 +7,8 @@ import { useUpdateHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOve import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import NotificationManager from "@/components/molecules/notifications_manager"; -import { testHashicorpVaultConnection } from "@/components/networking"; -import { Alert, Button, Card, Descriptions, Skeleton, Space, Typography } from "antd"; +import { testHashicorpVaultConnection } from "@/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi"; +import { Alert, Button, Card, Descriptions, Flex, Skeleton, Space, Typography } from "antd"; import { Edit, KeyRound, PlugZap, Trash2 } from "lucide-react"; import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants"; import EditHashicorpVaultModal from "./EditHashicorpVaultModal"; @@ -85,7 +85,7 @@ export default function HashicorpVault() { } if (SENSITIVE_FIELDS.has(key)) { return ( -
+ {value}
+ ); } return {value}; @@ -140,16 +140,16 @@ export default function HashicorpVault() { {/* Header */} -
-
+ +
Hashicorp Vault Manage secret manager configuration
-
+ -
+ {isConfigured && ( <>
-
+
+ {isConfigured && ( { - const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` - : `/config_overrides/hashicorp_vault`; - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - throw new Error(errorMessage); - } - const data = await response.json(); - return data; -}; - -export const updateHashicorpVaultConfig = async ( - accessToken: string, - config: Record, -) => { - const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` - : `/config_overrides/hashicorp_vault`; - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(config), - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - throw new Error(errorMessage); - } - const data = await response.json(); - return data; -}; - -export const deleteHashicorpVaultConfig = async (accessToken: string) => { - const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/config_overrides/hashicorp_vault` - : `/config_overrides/hashicorp_vault`; - const response = await fetch(url, { - method: "DELETE", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - throw new Error(errorMessage); - } - const data = await response.json(); - return data; -}; - -export const testHashicorpVaultConnection = async (accessToken: string) => { - const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/config_overrides/hashicorp_vault/test_connection` - : `/config_overrides/hashicorp_vault/test_connection`; - const response = await fetch(url, { - method: "POST", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - }, - }); - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - throw new Error(errorMessage); - } - const data = await response.json(); - return data; -}; // ============================================================ // Claude Code Marketplace Networking Functions From 2390ab2c2ca7d3ad15c34296bec4be129ed1c7a1 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 7 Mar 2026 11:04:01 -0800 Subject: [PATCH 6/6] fix: align detectAuthMethod priority with backend --- .../Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx index b923fafeb1e..33a2c9e22b5 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx @@ -17,8 +17,9 @@ import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder"; const { Title, Text } = Typography; function detectAuthMethod(values: Record): string { - if (values.vault_token) return "Token"; if (values.approle_role_id || values.approle_secret_id) return "AppRole"; + if (values.client_cert && values.client_key) return "TLS Certificate"; + if (values.vault_token) return "Token"; return "None"; }