diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 754c8591460..9012dee8fc0 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -514,6 +514,7 @@ class LiteLLMRoutes(enum.Enum): "/user/daily/activity", "/model/{model_id}/update", "/prompt/list", + "/prompt/info", ] # routes that manage their own allowed/disallowed logic ## Org Admin Routes ## diff --git a/litellm/proxy/management_endpoints/common_utils.py b/litellm/proxy/management_endpoints/common_utils.py index fcab831e3e3..4e22edd7eab 100644 --- a/litellm/proxy/management_endpoints/common_utils.py +++ b/litellm/proxy/management_endpoints/common_utils.py @@ -1,6 +1,7 @@ from typing import Any, Optional, Union from litellm.proxy._types import ( + GenerateKeyRequest, KeyRequestBase, LiteLLM_ManagementEndpoint_MetadataFields_Premium, LiteLLM_TeamTable, diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index ad483a5ac1e..191815b78ed 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -855,6 +855,15 @@ async def prepare_key_update_data( data: Union[UpdateKeyRequest, RegenerateKeyRequest], existing_key_row: LiteLLM_VerificationToken, ): + for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium: + if getattr(data, field, None) is not None: + _set_object_metadata_field( + object_data=data, + field_name=field, + value=getattr(data, field), + ) + delattr(data, field) + data_json: dict = data.model_dump(exclude_unset=True) data_json.pop("key", None) data_json.pop("new_key", None) diff --git a/litellm/proxy/prompts/prompt_endpoints.py b/litellm/proxy/prompts/prompt_endpoints.py index 70b969db5ff..2b8d533c70b 100644 --- a/litellm/proxy/prompts/prompt_endpoints.py +++ b/litellm/proxy/prompts/prompt_endpoints.py @@ -4,11 +4,11 @@ CRUD ENDPOINTS FOR PROMPTS from typing import List, Optional, cast -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException -from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.types.prompts.init_prompts import ListPromptsResponse +from litellm.types.prompts.init_prompts import ListPromptsResponse, PromptSpec router = APIRouter() @@ -50,3 +50,43 @@ async def list_prompts( ) else: return ListPromptsResponse(prompts=[]) + + +@router.get( + "/prompt/info", + tags=["Prompt Management"], + dependencies=[Depends(user_api_key_auth)], + response_model=PromptSpec, +) +async def get_prompt( + prompt_id: str, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get info about a prompt + """ + from litellm.proxy.prompts.prompt_registry import IN_MEMORY_PROMPT_REGISTRY + + ## CHECK IF USER HAS ACCESS TO PROMPT + prompts: Optional[List[str]] = None + if user_api_key_dict.metadata is not None: + prompts = cast( + Optional[List[str]], user_api_key_dict.metadata.get("prompts", None) + ) + if prompts is not None and prompt_id not in prompts: + raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found") + if user_api_key_dict.user_role is not None and ( + user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN + or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value + ): + pass + else: + raise HTTPException( + status_code=403, + detail=f"You are not authorized to access this prompt. Your role - {user_api_key_dict.user_role}, Your key's prompts - {prompts}", + ) + + prompt_spec = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(prompt_id) + if prompt_spec is None: + raise HTTPException(status_code=400, detail=f"Prompt {prompt_id} not found") + return prompt_spec diff --git a/litellm/proxy/prompts/prompt_registry.py b/litellm/proxy/prompts/prompt_registry.py index 29d029eb9ac..49a25998d66 100644 --- a/litellm/proxy/prompts/prompt_registry.py +++ b/litellm/proxy/prompts/prompt_registry.py @@ -6,7 +6,11 @@ from typing import Callable, Dict, Optional from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_prompt_management import CustomPromptManagement -from litellm.types.prompts.init_prompts import PromptLiteLLMParams, PromptSpec +from litellm.types.prompts.init_prompts import ( + PromptInfo, + PromptLiteLLMParams, + PromptSpec, +) prompt_initializer_registry = {} @@ -147,6 +151,7 @@ class InMemoryPromptRegistry: parsed_prompt = PromptSpec( prompt_id=prompt_id, litellm_params=litellm_params, + prompt_info=PromptInfo(prompt_type="config"), ) # store references to the prompt in memory diff --git a/litellm/types/prompts/init_prompts.py b/litellm/types/prompts/init_prompts.py index 3f0c96ed8e4..43489be131f 100644 --- a/litellm/types/prompts/init_prompts.py +++ b/litellm/types/prompts/init_prompts.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional from pydantic import BaseModel, ConfigDict from typing_extensions import Required, TypedDict @@ -12,6 +12,12 @@ class SupportedPromptIntegrations(str, Enum): CUSTOM = "custom" +class PromptInfo(BaseModel): + prompt_type: Literal["config", "db"] + + model_config = ConfigDict(extra="allow", protected_namespaces=()) + + class PromptLiteLLMParams(BaseModel): prompt_id: str prompt_integration: str @@ -22,7 +28,7 @@ class PromptLiteLLMParams(BaseModel): class PromptSpec(TypedDict, total=False): prompt_id: Required[str] litellm_params: Required[PromptLiteLLMParams] - prompt_info: Optional[Dict] + prompt_info: Optional[PromptInfo] created_at: Optional[datetime] updated_at: Optional[datetime] diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 9d03d3eff0d..2194bbaec48 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -32,6 +32,7 @@ import { } from "@/components/networking"; import { Organization } from "@/components/networking"; import GuardrailsPanel from "@/components/guardrails"; +import PromptsPanel from "@/components/prompts"; import TransformRequestPanel from "@/components/transform_request"; import { fetchUserModels } from "@/components/create_key_button"; import { fetchTeams } from "@/components/common_components/fetch_teams"; @@ -361,6 +362,11 @@ export default function CreateKeyPage() { accessToken={accessToken} userRole={userRole} /> + ) : page == "prompts" ? ( + ) : page == "transform-request" ? ( ) : page == "general-settings" ? ( diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 7cd6edf9a13..e7394525f48 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -89,6 +89,7 @@ const Sidebar: React.FC = ({ icon: , children: [ { key: "9", page: "caching", label: "Caching", icon: , roles: all_admin_roles }, + { key: "25", page: "prompts", label: "Prompts", icon: , roles: all_admin_roles }, { key: "10", page: "budgets", label: "Budgets", icon: , roles: all_admin_roles }, { key: "20", page: "transform-request", label: "API Playground", icon: , roles: [...all_admin_roles, ...internalUserRoles] }, { key: "19", page: "tag-management", label: "Tag Management", icon: , roles: all_admin_roles }, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index bc6db4c6adc..ca0a79485a4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -75,13 +75,19 @@ export interface Model { model_info: Object | null; } -interface PromptSpec { - prompt_id: string; - litellm_params: Object; - prompt_info: Object; +interface PromptInfo { + prompt_type: string; } -interface ListPromptsResponse { +export interface PromptSpec { + prompt_id: string; + litellm_params: Object; + prompt_info: PromptInfo; + created_at?: string + updated_at?: string +} + +export interface ListPromptsResponse { prompts: PromptSpec[]; } @@ -4917,6 +4923,31 @@ export const getPromptsList = async (accessToken: String) : Promise => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/prompt/info?prompt_id=${promptId}` : `/prompt/info?prompt_id=${promptId}`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to get prompt info:", error); + throw error; + } +}; + export const createGuardrailCall = async ( accessToken: string, guardrailData: any diff --git a/ui/litellm-dashboard/src/components/prompts.tsx b/ui/litellm-dashboard/src/components/prompts.tsx new file mode 100644 index 00000000000..b2ccc4d7f80 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from "react" +import { Card, Text } from "@tremor/react" +import { getPromptsList, PromptSpec, ListPromptsResponse } from "./networking" +import PromptTable from "./prompts/prompt_table" +import PromptInfoView from "./prompts/prompt_info" +import { isAdminRole } from "@/utils/roles" + +interface PromptsProps { + accessToken: string | null + userRole?: string +} + +const PromptsPanel: React.FC = ({ accessToken, userRole }) => { + const [promptsList, setPromptsList] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [selectedPromptId, setSelectedPromptId] = useState(null) + + const isAdmin = userRole ? isAdminRole(userRole) : false + + const fetchPrompts = async () => { + if (!accessToken) { + return + } + + setIsLoading(true) + try { + const response: ListPromptsResponse = await getPromptsList(accessToken) + console.log(`prompts: ${JSON.stringify(response)}`) + setPromptsList(response.prompts) + } catch (error) { + console.error("Error fetching prompts:", error) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + fetchPrompts() + }, [accessToken]) + + const handlePromptClick = (promptId: string) => { + setSelectedPromptId(promptId) + } + + return ( +
+ {selectedPromptId ? ( + setSelectedPromptId(null)} + accessToken={accessToken} + isAdmin={isAdmin} + /> + ) : ( + <> +
+ Prompts +
+ + + + )} +
+ ) +} + +export default PromptsPanel \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/prompts/README.md b/ui/litellm-dashboard/src/components/prompts/README.md new file mode 100644 index 00000000000..613b0bce096 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/README.md @@ -0,0 +1,73 @@ +# Prompts Component + +This component provides a view-only interface for viewing prompts in the LiteLLM dashboard, similar to the guardrails component. + +## Components + +### PromptsPanel (`prompts.tsx`) +- Main component that displays the prompts list +- Fetches prompts using the `getPromptsList` API call +- Handles loading states and error handling + +### PromptTable (`prompt_table.tsx`) +- Table component that displays prompts data +- Uses Tanstack Table for sorting and filtering +- Shows: Prompt ID, Created At, Updated At, Type +- Supports clicking on prompt IDs to open detailed view + +### PromptInfoView (`prompt_info.tsx`) +- Detail view component for individual prompts +- Shows comprehensive prompt information including metadata and parameters +- Three-tab interface: **Overview**, **Details** (admin-only), and **Raw JSON** +- **Overview**: Shows formatted prompt information with key details +- **Details**: Shows structured breakdown of all prompt data (admin users only) +- **Raw JSON**: Shows exactly what the API returns with copy-to-clipboard functionality +- Includes copy-to-clipboard functionality for prompt ID and raw JSON +- Similar structure to GuardrailInfoView component + +## Usage + +The component is integrated into the main application at: +- **Navigation**: Available in the left sidebar under "Experimental" > "Prompts" (admin role required) +- **Routing**: Accessible via `?page=prompts` URL parameter +- **API**: Uses `getPromptsList` and `getPromptInfo` functions from `networking.tsx` +- **Detail View**: Click any prompt ID to view detailed information + +## Props + +```typescript +interface PromptsProps { + accessToken: string | null + userRole?: string +} +``` + +## Data Structure + +The component expects prompts with the following structure: + +```typescript +interface PromptItem { + prompt_id?: string + prompt_name: string | null + prompt_info: Record + created_at?: string + updated_at?: string +} +``` + +## Integration + +The component is fully integrated into the main application: + +1. **Left Navigation**: Added to `leftnav.tsx` with `FileTextOutlined` icon +2. **Main Routing**: Added to `page.tsx` with proper routing logic +3. **Permissions**: Restricted to admin roles (same as guardrails) + +## Future Enhancements + +- ✅ Add prompt detail view (similar to GuardrailInfoView) - **COMPLETED** +- Add create/edit/delete functionality +- Add bulk operations +- Add search and filtering capabilities +- Add export functionality \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/prompts/index.ts b/ui/litellm-dashboard/src/components/prompts/index.ts new file mode 100644 index 00000000000..e58400be37f --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/index.ts @@ -0,0 +1,2 @@ +export { default as PromptTable } from "./prompt_table" +export { default as PromptInfoView } from "./prompt_info" diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_info.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_info.tsx new file mode 100644 index 00000000000..aa309429334 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_info.tsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect } from "react" +import { + Card, + Title, + Text, + Grid, + Badge, + Button as TremorButton, + Tab, + TabGroup, + TabList, + TabPanel, + TabPanels, +} from "@tremor/react" +import { Button, message, Tooltip } from "antd" +import { ArrowLeftIcon } from "@heroicons/react/outline" +import { getPromptInfo, PromptSpec } from "@/components/networking" +import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils" +import { CheckIcon, CopyIcon } from "lucide-react" + +export interface PromptInfoProps { + promptId: string + onClose: () => void + accessToken: string | null + isAdmin: boolean +} + +const PromptInfoView: React.FC = ({ promptId, onClose, accessToken, isAdmin }) => { + const [promptData, setPromptData] = useState(null) + const [rawApiResponse, setRawApiResponse] = useState(null) + const [loading, setLoading] = useState(true) + const [copiedStates, setCopiedStates] = useState>({}) + + const fetchPromptInfo = async () => { + try { + setLoading(true) + if (!accessToken) return + const response = await getPromptInfo(accessToken, promptId) + setPromptData(response) + setRawApiResponse(response) // Store the raw response for the Raw JSON tab + } catch (error) { + message.error("Failed to load prompt information") + console.error("Error fetching prompt info:", error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchPromptInfo() + }, [promptId, accessToken]) + + if (loading) { + return
Loading...
+ } + + if (!promptData) { + return
Prompt not found
+ } + + // Format date helper function + const formatDate = (dateString?: string) => { + if (!dateString) return "-" + const date = new Date(dateString) + return date.toLocaleString() + } + + const copyToClipboard = async (text: string | null | undefined, key: string) => { + const success = await utilCopyToClipboard(text) + if (success) { + setCopiedStates((prev) => ({ ...prev, [key]: true })) + setTimeout(() => { + setCopiedStates((prev) => ({ ...prev, [key]: false })) + }, 2000) + } + } + + return ( +
+
+ + Back to Prompts + + Prompt Details +
+ {promptData.prompt_id} +
+
+ + + + Overview + {isAdmin ? Details : <>} + Raw JSON + + + + {/* Overview Panel */} + + + + Prompt ID +
+ {promptData.prompt_id} +
+
+ + + Prompt Type +
+ {promptData.prompt_info?.prompt_type || "-"} + + {promptData.prompt_info?.prompt_type || "Unknown"} + +
+
+ + + Created At +
+ {formatDate(promptData.created_at)} + Last Updated: {formatDate(promptData.updated_at)} +
+
+
+ + {promptData.litellm_params && Object.keys(promptData.litellm_params).length > 0 && ( + + LiteLLM Parameters +
+
+                    {JSON.stringify(promptData.litellm_params, null, 2)}
+                  
+
+
+ )} +
+ + {/* Details Panel (only for admins) */} + {isAdmin && ( + + + Prompt Details +
+
+ Prompt ID +
{promptData.prompt_id}
+
+ +
+ Prompt Type +
{promptData.prompt_info?.prompt_type || "-"}
+
+ +
+ Created At +
{formatDate(promptData.created_at)}
+
+ +
+ Last Updated +
{formatDate(promptData.updated_at)}
+
+ +
+ LiteLLM Parameters +
+
+                        {JSON.stringify(promptData.litellm_params, null, 2)}
+                      
+
+
+ +
+ Prompt Info +
+
+                        {JSON.stringify(promptData.prompt_info, null, 2)}
+                      
+
+
+
+
+
+ )} + + {/* Raw JSON Panel */} + + +
+ Raw API Response + +
+ +
+
+                  {JSON.stringify(rawApiResponse, null, 2)}
+                
+
+
+
+
+
+
+ ) +} + +export default PromptInfoView \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_table.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_table.tsx new file mode 100644 index 00000000000..6c2417cd387 --- /dev/null +++ b/ui/litellm-dashboard/src/components/prompts/prompt_table.tsx @@ -0,0 +1,174 @@ +import React, { useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Button } from "@tremor/react" +import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline" +import { Tooltip } from "antd" +import {PromptSpec} from "@/components/networking" +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, +} from "@tanstack/react-table" + + +interface PromptTableProps { + promptsList: PromptSpec[] + isLoading: boolean + onPromptClick?: (id: string) => void +} + +const PromptTable: React.FC = ({ + promptsList, + isLoading, + onPromptClick, +}) => { + const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]) + + // Format date helper function + const formatDate = (dateString?: string) => { + if (!dateString) return "-" + const date = new Date(dateString) + return date.toLocaleString() + } + + const columns: ColumnDef[] = [ + { + header: "Prompt ID", + accessorKey: "prompt_id", + cell: (info: any) => ( + + + + ), + }, + { + header: "Created At", + accessorKey: "created_at", + cell: ({ row }) => { + const prompt = row.original + return ( + + {formatDate(prompt.created_at)} + + ) + }, + }, + { + header: "Updated At", + accessorKey: "updated_at", + cell: ({ row }) => { + const prompt = row.original + return ( + + {formatDate(prompt.updated_at)} + + ) + }, + }, + { + header: "Type", + accessorKey: "prompt_info.prompt_type", + cell: ({ row }) => { + const prompt = row.original + return ( + + {prompt.prompt_info.prompt_type} + + ) + }, + }, + ] + + const table = useReactTable({ + data: promptsList, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSorting: true, + }) + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+
+
+ ))} +
+ ))} +
+ + {isLoading ? ( + + +
+

Loading...

+
+
+
+ ) : promptsList.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No prompts found

+
+
+
+ )} +
+
+
+
+ ) +} + +export default PromptTable \ No newline at end of file