diff --git a/docs/my-website/docs/proxy/enterprise.md b/docs/my-website/docs/proxy/enterprise.md index c47589e8a..a8c84bf4f 100644 --- a/docs/my-website/docs/proxy/enterprise.md +++ b/docs/my-website/docs/proxy/enterprise.md @@ -21,6 +21,7 @@ Features: - ✅ Don't log/store specific requests to Langfuse, Sentry, etc. (eg confidential LLM requests) - ✅ Tracking Spend for Custom Tags - ✅ Custom Branding + Routes on Swagger Docs +- ✅ Audit Logs for `Created At, Created By` when Models Added ## Content Moderation diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 8d58f6017..9d6c62bdd 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2790,6 +2790,13 @@ class ProxyConfig: model.model_info["id"] = _id model.model_info["db_model"] = True + if premium_user is True: + # seeing "created_at", "updated_at", "created_by", "updated_by" is a LiteLLM Enterprise Feature + model.model_info["created_at"] = getattr(model, "created_at", None) + model.model_info["updated_at"] = getattr(model, "updated_at", None) + model.model_info["created_by"] = getattr(model, "created_by", None) + model.model_info["updated_by"] = getattr(model, "updated_by", None) + if model.model_info is not None and isinstance(model.model_info, dict): if "id" not in model.model_info: model.model_info["id"] = model.model_id @@ -3075,10 +3082,9 @@ class ProxyConfig: try: if master_key is None or not isinstance(master_key, str): - raise Exception( + raise ValueError( f"Master key is not initialized or formatted. master_key={master_key}" ) - verbose_proxy_logger.debug(f"llm_router: {llm_router}") new_models = await prisma_client.db.litellm_proxymodeltable.find_many() # update llm router await self._update_llm_router( diff --git a/litellm/types/router.py b/litellm/types/router.py index 93c65a1cf..75e792f4c 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -1,9 +1,15 @@ +""" + litellm.Router Types - includes RouterConfig, UpdateRouterConfig, ModelInfo etc +""" + from typing import List, Optional, Union, Dict, Tuple, Literal, TypedDict +import uuid +import enum import httpx -from pydantic import BaseModel, validator, Field +from pydantic import BaseModel, Field +import datetime from .completion import CompletionRequest from .embedding import EmbeddingRequest -import uuid, enum class ModelConfig(BaseModel): @@ -76,6 +82,12 @@ class ModelInfo(BaseModel): db_model: bool = ( False # used for proxy - to separate models which are stored in the db vs. config. ) + updated_at: Optional[datetime.datetime] = None + updated_by: Optional[str] = None + + created_at: Optional[datetime.datetime] = None + created_by: Optional[str] = None + base_model: Optional[str] = ( None # specify if the base model is azure/gpt-3.5-turbo etc for accurate cost tracking ) diff --git a/ui/litellm-dashboard/src/components/model_dashboard.tsx b/ui/litellm-dashboard/src/components/model_dashboard.tsx index 89061d916..2cb1c2cf0 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard.tsx @@ -86,6 +86,8 @@ import type { UploadProps } from "antd"; import { Upload } from "antd"; import TimeToFirstToken from "./model_metrics/time_to_first_token"; import DynamicFields from "./model_add/dynamic_form"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; + interface ModelDashboardProps { accessToken: string | null; token: string | null; @@ -269,6 +271,8 @@ const ModelDashboard: React.FC = ({ const [selectedProvider, setSelectedProvider] = useState("OpenAI"); const [healthCheckResponse, setHealthCheckResponse] = useState(""); const [editModalVisible, setEditModalVisible] = useState(false); + const [infoModalVisible, setInfoModalVisible] = useState(false); + const [selectedModel, setSelectedModel] = useState(null); const [availableModelGroups, setAvailableModelGroups] = useState< Array @@ -297,6 +301,15 @@ const ModelDashboard: React.FC = ({ useState(null); const [defaultRetry, setDefaultRetry] = useState(0); + function formatCreatedAt(createdAt: string | null) { + if (createdAt) { + const date = new Date(createdAt); + const options = { month: 'long', day: 'numeric', year: 'numeric' }; + return date.toLocaleDateString('en-US'); + } + return null; + } + const EditModelModal: React.FC = ({ visible, onCancel, @@ -423,11 +436,21 @@ const ModelDashboard: React.FC = ({ setEditModalVisible(true); }; + const handleInfoClick = (model: any) => { + setSelectedModel(model); + setInfoModalVisible(true); + }; + const handleEditCancel = () => { setEditModalVisible(false); setSelectedModel(null); }; + const handleInfoCancel = () => { + setInfoModalVisible(false); + setSelectedModel(null); + }; + const handleEditSubmit = async (formValues: Record) => { // Call API to update team with teamId and values @@ -1039,7 +1062,6 @@ const ModelDashboard: React.FC = ({ @@ -1049,6 +1071,7 @@ const ModelDashboard: React.FC = ({ maxWidth: "150px", whiteSpace: "normal", wordBreak: "break-word", + fontSize: "11px" }} > Public Model Name @@ -1058,6 +1081,7 @@ const ModelDashboard: React.FC = ({ maxWidth: "100px", whiteSpace: "normal", wordBreak: "break-word", + fontSize: "11px" }} > Provider @@ -1068,25 +1092,18 @@ const ModelDashboard: React.FC = ({ maxWidth: "150px", whiteSpace: "normal", wordBreak: "break-word", + fontSize: "11px" }} > API Base )} - - Extra litellm Params - Input Price{" "} @@ -1099,6 +1116,7 @@ const ModelDashboard: React.FC = ({ maxWidth: "85px", whiteSpace: "normal", wordBreak: "break-word", + fontSize: "11px" }} > Output Price{" "} @@ -1106,24 +1124,45 @@ const ModelDashboard: React.FC = ({ /1M Tokens ($)

+ - Max Tokens + { + premiumUser ? "Created At" : ✨ Created At + } + + + + { + premiumUser ? "Created By" : ✨ Created By + } Status + + +
@@ -1137,15 +1176,17 @@ const ModelDashboard: React.FC = ({ selectedModelGroup === "" ) .map((model: any, index: number) => ( - + - {model.model_name} +

+ {model.model_name || "-"} +

= ({ wordBreak: "break-word", }} > - {model.provider} +

+ {model.provider || "-"} +

{userRole === "Admin" && ( + +
-                              {model.api_base}
-                            
+                              {model && model.api_base ? model.api_base.slice(0, 20) : "-"}
+                            
+
+
+ )} - - - - Litellm params - - -
-                                  {JSON.stringify(
-                                    model.cleanedLitellmParams,
-                                    null,
-                                    2
-                                  )}
-                                
-
-
-
= ({ wordBreak: "break-word", }} > +
                             {model.input_cost ||
                               model.litellm_params.input_cost_per_token ||
-                              null}
+                              "-"}
+                            
= ({ wordBreak: "break-word", }} > +
                             {model.output_cost ||
                               model.litellm_params.output_cost_per_token ||
-                              null}
+                              "-"}
+                            
- -

- Max Tokens: {model.max_tokens}

- Max Input Tokens: {model.max_input_tokens} + +

+ { + premiumUser ? formatCreatedAt(model.model_info.created_at) || "-" : "-" + } +

+ +
+ +

+ { + premiumUser ? model.model_info.created_by || "-" : "-" + }

= ({ size="xs" className="text-white" > -

DB Model

+

DB Model

) : ( = ({ size="xs" className="text-black" > -

Config Model

+

Config Model

)}
+ +
+ handleInfoClick(model)} + /> + + handleEditClick(model)} /> + + + + + + + ))} @@ -1277,6 +1334,20 @@ const ModelDashboard: React.FC = ({ model={selectedModel} onSubmit={handleEditSubmit} /> + + + Model Info + + {selectedModel && JSON.stringify(selectedModel, null, 2)} + + + Add new model