= {};
+ for (const { key, value } of customHeaders) {
+ if (key.trim()) headersObj[key.trim()] = value;
+ }
+ try {
+ await updateGuardrailCall(accessToken, id, {
+ litellm_params: { headers: headersObj },
+ });
+ setGuardrails((prev) =>
+ prev.map((x) =>
+ x.id === id
+ ? {
+ ...x,
+ customHeaders: customHeaders.filter((h) => h.key.trim()),
+ }
+ : x
+ )
+ );
+ NotificationsManager.success("Static headers updated");
+ } catch {
+ NotificationsManager.fromBackend("Failed to update static headers");
+ }
+ }
+
+ async function updateExtraHeaders(id: string, extraHeaders: string[]) {
+ if (!accessToken) return;
+ try {
+ await updateGuardrailCall(accessToken, id, {
+ litellm_params: { extra_headers: extraHeaders },
+ });
+ setGuardrails((prev) =>
+ prev.map((x) => (x.id === id ? { ...x, extraHeaders } : x))
+ );
+ NotificationsManager.success("Forward client headers updated");
+ } catch {
+ NotificationsManager.fromBackend("Failed to update forward client headers");
+ }
+ }
+
+ async function handleApprove(id: string) {
+ if (!accessToken) return;
+ try {
+ await approveGuardrailSubmission(accessToken, id);
+ setConfirmAction(null);
+ if (selectedId === id) setSelectedId(null);
+ await fetchSubmissions();
+ NotificationsManager.success("Guardrail approved");
+ } catch {
+ NotificationsManager.fromBackend("Failed to approve guardrail");
+ }
+ }
+
+ async function handleReject(id: string) {
+ if (!accessToken) return;
+ try {
+ await rejectGuardrailSubmission(accessToken, id);
+ setConfirmAction(null);
+ if (selectedId === id) setSelectedId(null);
+ await fetchSubmissions();
+ NotificationsManager.success("Guardrail rejected");
+ } catch {
+ NotificationsManager.fromBackend("Failed to reject guardrail");
+ }
+ }
+
+ function toggleHeaders(id: string) {
+ setExpandedHeaders((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
+ return next;
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ className="w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
+ />
+
+
+ setStatusFilter(e.target.value as typeof statusFilter)
+ }
+ className="border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
+ >
+ All Status
+ Pending Review
+ Active
+ Rejected
+
+
+
+ Add Guardrail
+
+
+
+ {isLoading && (
+
+ Loading submissions…
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+ {!isLoading && !error && filtered.length === 0 && (
+
+ No guardrails match your filters.
+
+ )}
+ {!isLoading && !error && filtered.map((g) => (
+
setSelectedId(selectedId === g.id ? null : g.id)}
+ onToggleForwardKey={() => toggleForwardKey(g.id)}
+ onToggleHeaders={() => toggleHeaders(g.id)}
+ onApprove={() => setConfirmAction({ id: g.id, action: "approve" })}
+ onReject={() => setConfirmAction({ id: g.id, action: "reject" })}
+ />
+ ))}
+
+
+ {selected && (
+
setSelectedId(null)}
+ onApprove={() =>
+ setConfirmAction({ id: selected.id, action: "approve" })
+ }
+ onReject={() =>
+ setConfirmAction({ id: selected.id, action: "reject" })
+ }
+ onToggleForwardKey={() => toggleForwardKey(selected.id)}
+ onUpdateCustomHeaders={(customHeaders) =>
+ updateCustomHeaders(selected.id, customHeaders)
+ }
+ onUpdateExtraHeaders={(extraHeaders) =>
+ updateExtraHeaders(selected.id, extraHeaders)
+ }
+ />
+ )}
+ {confirmAction && (
+ g.id === confirmAction.id)?.name ?? ""
+ }
+ onConfirm={() =>
+ confirmAction.action === "approve"
+ ? handleApprove(confirmAction.id)
+ : handleReject(confirmAction.id)
+ }
+ onCancel={() => setConfirmAction(null)}
+ />
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx
index e2fc8caa21c..40c1a3a386a 100644
--- a/ui/litellm-dashboard/src/components/model_info_view.tsx
+++ b/ui/litellm-dashboard/src/components/model_info_view.tsx
@@ -17,6 +17,7 @@ import {
Button as TremorButton,
} from "@tremor/react";
import { Button, Form, Input, Modal, Select, Tooltip } from "antd";
+import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
@@ -245,6 +246,11 @@ export default function ModelInfoView({
if (values.guardrails) {
updatedLitellmParams.guardrails = values.guardrails;
}
+ if (values.vector_store_ids !== undefined) {
+ updatedLitellmParams.vector_store_ids = Array.isArray(values.vector_store_ids)
+ ? values.vector_store_ids
+ : [];
+ }
// Handle cache control settings
if (values.cache_control && values.cache_control_injection_points?.length > 0) {
@@ -606,6 +612,9 @@ export default function ModelInfoView({
guardrails: Array.isArray(localModelData.litellm_params?.guardrails)
? localModelData.litellm_params.guardrails
: [],
+ vector_store_ids: Array.isArray(localModelData.litellm_params?.vector_store_ids)
+ ? localModelData.litellm_params.vector_store_ids
+ : [],
tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [],
health_check_model: isWildcardModel ? localModelData.model_info?.health_check_model : null,
litellm_extra_params: JSON.stringify(localModelData.litellm_params || {}, null, 2),
@@ -883,6 +892,58 @@ export default function ModelInfoView({
)}
+
+
+ Attached Knowledge Bases (RAG)
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ {isEditing ? (
+
+ {}}
+ accessToken={accessToken || ""}
+ placeholder="Select knowledge bases (optional)"
+ />
+
+ ) : (
+
+ {localModelData.litellm_params?.vector_store_ids ? (
+ Array.isArray(localModelData.litellm_params.vector_store_ids) ? (
+ localModelData.litellm_params.vector_store_ids.length > 0 ? (
+
+ {localModelData.litellm_params.vector_store_ids.map(
+ (vsId: string, index: number) => (
+
+ {vsId}
+
+ )
+ )}
+
+ ) : (
+ "No knowledge bases attached"
+ )
+ ) : (
+ String(localModelData.litellm_params.vector_store_ids)
+ )
+ ) : (
+ "Not Set"
+ )}
+
+ )}
+
+
Tags
{isEditing ? (
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index df5b048952b..f3653a30060 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -5484,6 +5484,131 @@ export const getGuardrailsList = async (accessToken: string) => {
}
};
+// Team guardrail submissions (admin)
+export interface GuardrailSubmissionItem {
+ guardrail_id: string;
+ guardrail_name: string;
+ status: string; // "pending_review" | "active" | "rejected"
+ team_id?: string | null;
+ team_guardrail?: boolean; // true when submitted via team (team_id set)
+ litellm_params?: Record | null;
+ guardrail_info?: Record | null;
+ submitted_by_user_id?: string | null;
+ submitted_by_email?: string | null;
+ submitted_at?: string | null;
+ reviewed_at?: string | null;
+ created_at?: string | null;
+ updated_at?: string | null;
+}
+
+export interface GuardrailSubmissionSummary {
+ total: number;
+ pending_review: number;
+ active: number;
+ rejected: number;
+}
+
+export interface ListGuardrailSubmissionsResponse {
+ submissions: GuardrailSubmissionItem[];
+ summary: GuardrailSubmissionSummary;
+}
+
+export const listGuardrailSubmissions = async (
+ accessToken: string,
+ params?: { status?: string; team_id?: string; team_guardrail?: boolean; search?: string }
+): Promise => {
+ const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/submissions` : `/guardrails/submissions`;
+ const searchParams = new URLSearchParams();
+ if (params?.status) searchParams.set("status", params.status);
+ if (params?.team_id) searchParams.set("team_id", params.team_id);
+ if (params?.team_guardrail !== undefined) searchParams.set("team_guardrail", String(params.team_guardrail));
+ if (params?.search) searchParams.set("search", params.search);
+ const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
+ const response = await fetch(fullUrl, {
+ method: "GET",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = deriveErrorMessage(errorData);
+ handleError(errorMessage);
+ throw new Error(errorMessage);
+ }
+ return response.json();
+};
+
+export const getGuardrailSubmission = async (
+ accessToken: string,
+ guardrailId: string
+): Promise => {
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}`
+ : `/guardrails/submissions/${encodeURIComponent(guardrailId)}`;
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = deriveErrorMessage(errorData);
+ handleError(errorMessage);
+ throw new Error(errorMessage);
+ }
+ return response.json();
+};
+
+export const approveGuardrailSubmission = async (
+ accessToken: string,
+ guardrailId: string
+): Promise<{ guardrail_id: string; status: string; message: string }> => {
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/approve`
+ : `/guardrails/submissions/${encodeURIComponent(guardrailId)}/approve`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = deriveErrorMessage(errorData);
+ handleError(errorMessage);
+ throw new Error(errorMessage);
+ }
+ return response.json();
+};
+
+export const rejectGuardrailSubmission = async (
+ accessToken: string,
+ guardrailId: string
+): Promise<{ guardrail_id: string; status: string; message: string }> => {
+ const url = proxyBaseUrl
+ ? `${proxyBaseUrl}/guardrails/submissions/${encodeURIComponent(guardrailId)}/reject`
+ : `/guardrails/submissions/${encodeURIComponent(guardrailId)}/reject`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ const errorMessage = deriveErrorMessage(errorData);
+ handleError(errorMessage);
+ throw new Error(errorMessage);
+ }
+ return response.json();
+};
+
// Guardrails / Policies usage (dashboard)
export const getGuardrailsUsageOverview = async (
accessToken: string,
@@ -8364,6 +8489,7 @@ export const updateGuardrailCall = async (
guardrail_name?: string;
default_on?: boolean;
guardrail_info?: Record;
+ litellm_params?: Record;
},
) => {
try {
diff --git a/ui/litellm-dashboard/src/contexts/ReactQueryProvider.tsx b/ui/litellm-dashboard/src/contexts/ReactQueryProvider.tsx
new file mode 100644
index 00000000000..cf2d203a824
--- /dev/null
+++ b/ui/litellm-dashboard/src/contexts/ReactQueryProvider.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+const queryClient = new QueryClient();
+
+export default function ReactQueryProvider({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}