diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py
index 18af6399180..a369b7f3e36 100644
--- a/litellm/litellm_core_utils/litellm_logging.py
+++ b/litellm/litellm_core_utils/litellm_logging.py
@@ -719,25 +719,9 @@ class Logging(LiteLLMLoggingBaseClass):
Masks the headers of the request sent from LiteLLM
"""
- sensitive_keywords = [
- "authorization",
- "token",
- "key",
- "secret",
- ]
- return {
- k: (
- (v[:-44] + "*" * 44)
- if (isinstance(v, str) and len(v) > 44)
- else "*****"
- )
- for k, v in headers.items()
- if not ignore_sensitive_headers
- or not any(
- sensitive_keyword in k.lower()
- for sensitive_keyword in sensitive_keywords
- )
- }
+ return _get_masked_values(
+ headers, ignore_sensitive_values=ignore_sensitive_headers
+ )
def post_call(
self, original_response, input=None, api_key=None, additional_args={}
@@ -2413,6 +2397,58 @@ class Logging(LiteLLMLoggingBaseClass):
return result
+def _get_masked_values(
+ sensitive_object: dict,
+ ignore_sensitive_values: bool = False,
+ mask_all_values: bool = False,
+ unmasked_length: int = 4,
+ number_of_asterisks: Optional[int] = 4,
+) -> dict:
+ """
+ Internal debugging helper function
+
+ Masks the headers of the request sent from LiteLLM
+
+ Args:
+ masked_length: Optional length for the masked portion (number of *). If set, will use exactly this many *
+ regardless of original string length. The total length will be unmasked_length + masked_length.
+ """
+ sensitive_keywords = [
+ "authorization",
+ "token",
+ "key",
+ "secret",
+ ]
+ return {
+ k: (
+ (
+ v[: unmasked_length // 2]
+ + "*" * number_of_asterisks
+ + v[-unmasked_length // 2 :]
+ )
+ if (
+ isinstance(v, str)
+ and len(v) > unmasked_length
+ and number_of_asterisks is not None
+ )
+ else (
+ (
+ v[: unmasked_length // 2]
+ + "*" * (len(v) - unmasked_length)
+ + v[-unmasked_length // 2 :]
+ )
+ if (isinstance(v, str) and len(v) > unmasked_length)
+ else "*****"
+ )
+ )
+ for k, v in sensitive_object.items()
+ if not ignore_sensitive_values
+ or not any(
+ sensitive_keyword in k.lower() for sensitive_keyword in sensitive_keywords
+ )
+ }
+
+
def set_callbacks(callback_list, function_id=None): # noqa: PLR0915
"""
Globally sets the callback client
diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html
deleted file mode 100644
index 95f6319f7ac..00000000000
--- a/litellm/proxy/_experimental/out/onboarding.html
+++ /dev/null
@@ -1 +0,0 @@
-
LiteLLM Dashboard
\ No newline at end of file
diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py
index 84b35e3c522..bbbbbfd6b74 100644
--- a/litellm/proxy/credential_endpoints/endpoints.py
+++ b/litellm/proxy/credential_endpoints/endpoints.py
@@ -2,16 +2,19 @@
CRUD endpoints for storing reusable credentials.
"""
+from typing import Optional
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
+from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
from litellm.proxy.utils import handle_exception_on_proxy, jsonify_object
-from litellm.types.utils import CredentialItem
+from litellm.types.utils import CreateCredentialItem, CredentialItem
router = APIRouter()
@@ -35,7 +38,7 @@ class CredentialHelperUtils:
async def create_credential(
request: Request,
fastapi_response: Response,
- credential: CredentialItem,
+ credential: CreateCredentialItem,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@@ -43,7 +46,7 @@ async def create_credential(
Stores credential in DB.
Reloads credentials in memory.
"""
- from litellm.proxy.proxy_server import prisma_client
+ from litellm.proxy.proxy_server import llm_router, prisma_client
try:
if prisma_client is None:
@@ -51,9 +54,35 @@ async def create_credential(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
+ if credential.model_id:
+ if llm_router is None:
+ raise HTTPException(
+ status_code=500,
+ detail="LLM router not found. Please ensure you have a valid router instance.",
+ )
+ # get model from router
+ model = llm_router.get_deployment(credential.model_id)
+ if model is None:
+ raise HTTPException(status_code=404, detail="Model not found")
+ credential_values = llm_router.get_deployment_credentials(
+ credential.model_id
+ )
+ if credential_values is None:
+ raise HTTPException(status_code=404, detail="Model not found")
+ credential.credential_values = credential_values
+ if credential.credential_values is None:
+ raise HTTPException(
+ status_code=400,
+ detail="Credential values are required. Unable to infer credential values from model ID.",
+ )
+ processed_credential = CredentialItem(
+ credential_name=credential.credential_name,
+ credential_values=credential.credential_values,
+ credential_info=credential.credential_info,
+ )
encrypted_credential = CredentialHelperUtils.encrypt_credential_values(
- credential
+ processed_credential
)
credentials_dict = encrypted_credential.model_dump()
credentials_dict_jsonified = jsonify_object(credentials_dict)
@@ -66,7 +95,7 @@ async def create_credential(
)
## ADD TO LITELLM ##
- CredentialAccessor.upsert_credentials([credential])
+ CredentialAccessor.upsert_credentials([processed_credential])
return {"success": True, "message": "Credential created successfully"}
except Exception as e:
@@ -91,6 +120,7 @@ async def get_credentials(
masked_credentials = [
{
"credential_name": credential.credential_name,
+ "credential_values": _get_masked_values(credential.credential_values),
"credential_info": credential.credential_info,
}
for credential in litellm.credential_list
@@ -101,30 +131,75 @@ async def get_credentials(
@router.get(
- "/credentials/{credential_name}",
+ "/credentials/by_name/{credential_name}",
dependencies=[Depends(user_api_key_auth)],
tags=["credential management"],
+ response_model=CredentialItem,
+)
+@router.get(
+ "/credentials/by_model/{model_id}",
+ dependencies=[Depends(user_api_key_auth)],
+ tags=["credential management"],
+ response_model=CredentialItem,
)
async def get_credential(
request: Request,
fastapi_response: Response,
- credential_name: str,
+ credential_name: Optional[str] = None,
+ model_id: Optional[str] = None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
[BETA] endpoint. This might change unexpectedly.
"""
+ from litellm.proxy.proxy_server import llm_router
+
try:
- for credential in litellm.credential_list:
- if credential.credential_name == credential_name:
- masked_credential = {
- "credential_name": credential.credential_name,
- "credential_values": credential.credential_values,
- }
- return {"success": True, "credential": masked_credential}
- return {"success": False, "message": "Credential not found"}
+ if model_id:
+ if llm_router is None:
+ raise HTTPException(status_code=500, detail="LLM router not found")
+ model = llm_router.get_deployment(model_id)
+ if model is None:
+ raise HTTPException(status_code=404, detail="Model not found")
+ credential_values = llm_router.get_deployment_credentials(model_id)
+ if credential_values is None:
+ raise HTTPException(status_code=404, detail="Model not found")
+ masked_credential_values = _get_masked_values(
+ credential_values,
+ unmasked_length=4,
+ number_of_asterisks=4,
+ )
+ credential = CredentialItem(
+ credential_name="{}-credential-{}".format(model.model_name, model_id),
+ credential_values=masked_credential_values,
+ credential_info={},
+ )
+ # return credential object
+ return credential
+ elif credential_name:
+ for credential in litellm.credential_list:
+ if credential.credential_name == credential_name:
+ masked_credential = CredentialItem(
+ credential_name=credential.credential_name,
+ credential_values=_get_masked_values(
+ credential.credential_values,
+ unmasked_length=4,
+ number_of_asterisks=4,
+ ),
+ credential_info=credential.credential_info,
+ )
+ return masked_credential
+ raise HTTPException(
+ status_code=404,
+ detail="Credential not found. Got credential name: " + credential_name,
+ )
+ else:
+ raise HTTPException(
+ status_code=404, detail="Credential name or model ID required"
+ )
except Exception as e:
- return handle_exception_on_proxy(e)
+ verbose_proxy_logger.exception(e)
+ raise handle_exception_on_proxy(e)
@router.delete(
@@ -164,7 +239,45 @@ async def delete_credential(
return handle_exception_on_proxy(e)
-@router.put(
+def update_db_credential(
+ db_credential: CredentialItem, updated_patch: CredentialItem
+) -> CredentialItem:
+ """
+ Update a credential in the DB.
+ """
+ merged_credential = CredentialItem(
+ credential_name=db_credential.credential_name,
+ credential_info=db_credential.credential_info,
+ credential_values=db_credential.credential_values,
+ )
+
+ encrypted_credential = CredentialHelperUtils.encrypt_credential_values(
+ updated_patch
+ )
+ # update model name
+ if encrypted_credential.credential_name:
+ merged_credential.credential_name = encrypted_credential.credential_name
+
+ # update litellm params
+ if encrypted_credential.credential_values:
+ # Encrypt any sensitive values
+ encrypted_params = {
+ k: v for k, v in encrypted_credential.credential_values.items()
+ }
+
+ merged_credential.credential_values.update(encrypted_params)
+
+ # update model info
+ if encrypted_credential.credential_info:
+ """Update credential info"""
+ if "credential_info" not in merged_credential.credential_info:
+ merged_credential.credential_info = {}
+ merged_credential.credential_info.update(encrypted_credential.credential_info)
+
+ return merged_credential
+
+
+@router.patch(
"/credentials/{credential_name}",
dependencies=[Depends(user_api_key_auth)],
tags=["credential management"],
@@ -187,7 +300,13 @@ async def update_credential(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
- credential_object_jsonified = jsonify_object(credential.model_dump())
+ db_credential = await prisma_client.db.litellm_credentialstable.find_unique(
+ where={"credential_name": credential_name},
+ )
+ if db_credential is None:
+ raise HTTPException(status_code=404, detail="Credential not found in DB.")
+ merged_credential = update_db_credential(db_credential, credential)
+ credential_object_jsonified = jsonify_object(merged_credential.model_dump())
await prisma_client.db.litellm_credentialstable.update(
where={"credential_name": credential_name},
data={
diff --git a/litellm/router.py b/litellm/router.py
index f7f361354b2..a395c851dd8 100644
--- a/litellm/router.py
+++ b/litellm/router.py
@@ -111,6 +111,7 @@ from litellm.types.router import (
AlertingConfig,
AllowedFailsPolicy,
AssistantsTypedDict,
+ CredentialLiteLLMParams,
CustomRoutingStrategyBase,
Deployment,
DeploymentTypedDict,
@@ -636,29 +637,6 @@ class Router:
if self.cache.redis_cache is None:
self.cache.redis_cache = cache
- def initialize_assistants_endpoint(self):
- ## INITIALIZE PASS THROUGH ASSISTANTS ENDPOINT ##
- self.acreate_assistants = self.factory_function(litellm.acreate_assistants)
- self.adelete_assistant = self.factory_function(litellm.adelete_assistant)
- self.aget_assistants = self.factory_function(litellm.aget_assistants)
- self.acreate_thread = self.factory_function(litellm.acreate_thread)
- self.aget_thread = self.factory_function(litellm.aget_thread)
- self.a_add_message = self.factory_function(litellm.a_add_message)
- self.aget_messages = self.factory_function(litellm.aget_messages)
- self.arun_thread = self.factory_function(litellm.arun_thread)
-
- def initialize_router_endpoints(self):
- self.amoderation = self.factory_function(
- litellm.amoderation, call_type="moderation"
- )
- self.aanthropic_messages = self.factory_function(
- litellm.anthropic_messages, call_type="anthropic_messages"
- )
- self.aresponses = self.factory_function(
- litellm.aresponses, call_type="aresponses"
- )
- self.responses = self.factory_function(litellm.responses, call_type="responses")
-
def routing_strategy_init(
self, routing_strategy: Union[RoutingStrategy, str], routing_strategy_args: dict
):
@@ -724,6 +702,29 @@ class Router:
else:
pass
+ def initialize_assistants_endpoint(self):
+ ## INITIALIZE PASS THROUGH ASSISTANTS ENDPOINT ##
+ self.acreate_assistants = self.factory_function(litellm.acreate_assistants)
+ self.adelete_assistant = self.factory_function(litellm.adelete_assistant)
+ self.aget_assistants = self.factory_function(litellm.aget_assistants)
+ self.acreate_thread = self.factory_function(litellm.acreate_thread)
+ self.aget_thread = self.factory_function(litellm.aget_thread)
+ self.a_add_message = self.factory_function(litellm.a_add_message)
+ self.aget_messages = self.factory_function(litellm.aget_messages)
+ self.arun_thread = self.factory_function(litellm.arun_thread)
+
+ def initialize_router_endpoints(self):
+ self.amoderation = self.factory_function(
+ litellm.amoderation, call_type="moderation"
+ )
+ self.aanthropic_messages = self.factory_function(
+ litellm.anthropic_messages, call_type="anthropic_messages"
+ )
+ self.aresponses = self.factory_function(
+ litellm.aresponses, call_type="aresponses"
+ )
+ self.responses = self.factory_function(litellm.responses, call_type="responses")
+
def validate_fallbacks(self, fallback_param: Optional[List]):
"""
Validate the fallbacks parameter.
@@ -4625,6 +4626,17 @@ class Router:
raise Exception("Model invalid format - {}".format(type(model)))
return None
+ def get_deployment_credentials(self, model_id: str) -> Optional[dict]:
+ """
+ Returns -> dict of credentials for a given model id
+ """
+ deployment = self.get_deployment(model_id=model_id)
+ if deployment is None:
+ return None
+ return CredentialLiteLLMParams(
+ **deployment.litellm_params.model_dump(exclude_none=True)
+ ).model_dump(exclude_none=True)
+
def get_deployment_by_model_group_name(
self, model_group_name: str
) -> Optional[Deployment]:
diff --git a/litellm/types/completion.py b/litellm/types/completion.py
index 7b5ed4e502b..b06bb733c4f 100644
--- a/litellm/types/completion.py
+++ b/litellm/types/completion.py
@@ -1,6 +1,6 @@
from typing import Iterable, List, Optional, Union
-from pydantic import BaseModel, ConfigDict, validator
+from pydantic import BaseModel, ConfigDict
from typing_extensions import Literal, Required, TypedDict
diff --git a/litellm/types/router.py b/litellm/types/router.py
index 9a5fb168dac..e34366aa229 100644
--- a/litellm/types/router.py
+++ b/litellm/types/router.py
@@ -144,7 +144,26 @@ class ModelInfo(BaseModel):
setattr(self, key, value)
-class GenericLiteLLMParams(BaseModel):
+class CredentialLiteLLMParams(BaseModel):
+ api_key: Optional[str] = None
+ api_base: Optional[str] = None
+ api_version: Optional[str] = None
+ ## VERTEX AI ##
+ vertex_project: Optional[str] = None
+ vertex_location: Optional[str] = None
+ vertex_credentials: Optional[Union[str, dict]] = None
+ ## UNIFIED PROJECT/REGION ##
+ region_name: Optional[str] = None
+
+ ## AWS BEDROCK / SAGEMAKER ##
+ aws_access_key_id: Optional[str] = None
+ aws_secret_access_key: Optional[str] = None
+ aws_region_name: Optional[str] = None
+ ## IBM WATSONX ##
+ watsonx_region_name: Optional[str] = None
+
+
+class GenericLiteLLMParams(CredentialLiteLLMParams):
"""
LiteLLM Params without 'model' arg (used across completion / assistants api)
"""
@@ -152,9 +171,6 @@ class GenericLiteLLMParams(BaseModel):
custom_llm_provider: Optional[str] = None
tpm: Optional[int] = None
rpm: Optional[int] = None
- api_key: Optional[str] = None
- api_base: Optional[str] = None
- api_version: Optional[str] = None
timeout: Optional[Union[float, str, httpx.Timeout]] = (
None # if str, pass in as os.environ/
)
@@ -167,18 +183,7 @@ class GenericLiteLLMParams(BaseModel):
## LOGGING PARAMS ##
litellm_trace_id: Optional[str] = None
- ## UNIFIED PROJECT/REGION ##
- region_name: Optional[str] = None
- ## VERTEX AI ##
- vertex_project: Optional[str] = None
- vertex_location: Optional[str] = None
- vertex_credentials: Optional[Union[str, dict]] = None
- ## AWS BEDROCK / SAGEMAKER ##
- aws_access_key_id: Optional[str] = None
- aws_secret_access_key: Optional[str] = None
- aws_region_name: Optional[str] = None
- ## IBM WATSONX ##
- watsonx_region_name: Optional[str] = None
+
## CUSTOM PRICING ##
input_cost_per_token: Optional[float] = None
output_cost_per_token: Optional[float] = None
@@ -245,7 +250,11 @@ class GenericLiteLLMParams(BaseModel):
args.pop("__class__", None)
if max_retries is not None and isinstance(max_retries, str):
max_retries = int(max_retries) # cast to int
- super().__init__(max_retries=max_retries, **args, **params)
+ # We need to keep max_retries in args since it's a parameter of GenericLiteLLMParams
+ args["max_retries"] = (
+ max_retries # Put max_retries back in args after popping it
+ )
+ super().__init__(**args, **params)
def __contains__(self, key):
# Define custom behavior for the 'in' operator
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index 9608c099a33..a2d41d8fb9d 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -18,7 +18,7 @@ from openai.types.moderation import (
CategoryScores,
)
from openai.types.moderation_create_response import Moderation, ModerationCreateResponse
-from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
+from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from typing_extensions import Callable, Dict, Required, TypedDict, override
import litellm
@@ -2053,7 +2053,22 @@ class RawRequestTypedDict(TypedDict, total=False):
error: Optional[str]
-class CredentialItem(BaseModel):
+class CredentialBase(BaseModel):
credential_name: str
- credential_values: dict
credential_info: dict
+
+
+class CredentialItem(CredentialBase):
+ credential_values: dict
+
+
+class CreateCredentialItem(CredentialBase):
+ credential_values: Optional[dict] = None
+ model_id: Optional[str] = None
+
+ @model_validator(mode="before")
+ @classmethod
+ def check_credential_params(cls, values):
+ if not values.get("credential_values") and not values.get("model_id"):
+ raise ValueError("Either credential_values or model_id must be set")
+ return values
diff --git a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
index b7565b0494f..62fd3e4eb6d 100644
--- a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx
@@ -3,132 +3,392 @@ import { Form, Select } from "antd";
import { TextInput, Text } from "@tremor/react";
import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd";
import { UploadOutlined } from "@ant-design/icons";
-import { Providers } from "../provider_info_helpers";
+import { provider_map, Providers } from "../provider_info_helpers";
+import { CredentialItem } from "../networking";
const { Link } = Typography;
+
interface ProviderSpecificFieldsProps {
selectedProvider: Providers;
uploadProps?: UploadProps;
}
+interface ProviderCredentialField {
+ key: string;
+ label: string;
+ placeholder?: string;
+ tooltip?: string;
+ required?: boolean;
+ type?: "text" | "password" | "select" | "upload";
+ options?: string[];
+ defaultValue?: string;
+}
+
+export interface CredentialValues {
+ key: string;
+ value: string;
+}
+
+
+export const createCredentialFromModel = (provider: string, modelData: any): CredentialItem => {
+ console.log("provider", provider);
+ console.log("modelData", modelData);
+ const enumKey = Object.keys(provider_map).find(
+ key => provider_map[key].toLowerCase() === provider.toLowerCase()
+ );
+ if (!enumKey) {
+ throw new Error(`Provider ${provider} not found in provider_map`);
+ }
+ const providerEnum = Providers[enumKey as keyof typeof Providers];
+ const providerFields = PROVIDER_CREDENTIAL_FIELDS[providerEnum] || [];
+ const credentialValues: object = {};
+
+ console.log("providerFields", providerFields);
+
+ // Go through each field defined for this provider
+ providerFields.forEach(field => {
+ const value = modelData.litellm_params[field.key];
+ console.log("field", field);
+ console.log("value", value);
+ if (value !== undefined) {
+ (credentialValues as Record)[field.key] = value.toString();
+ }
+ });
+
+ const credential: CredentialItem = {
+ credential_name: `${provider}-credential-${Math.floor(Math.random() * 1000000)}`,
+ credential_values: credentialValues,
+ credential_info: {
+ custom_llm_provider: provider,
+ description: `Credential for ${provider}. Created from model ${modelData.model_name}`,
+ }
+ }
+
+ return credential;
+};
+
+const PROVIDER_CREDENTIAL_FIELDS: Record = {
+ [Providers.OpenAI]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ type: "select",
+ options: [
+ "https://api.openai.com/v1",
+ "https://eu.api.openai.com"
+ ],
+ defaultValue: "https://api.openai.com/v1"
+ },
+ {
+ key: "organization",
+ label: "OpenAI Organization ID",
+ placeholder: "[OPTIONAL] my-unique-org"
+ },
+ {
+ key: "api_key",
+ label: "OpenAI API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.OpenAI_Text]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ type: "select",
+ options: [
+ "https://api.openai.com/v1",
+ "https://eu.api.openai.com"
+ ],
+ defaultValue: "https://api.openai.com/v1"
+ },
+ {
+ key: "organization",
+ label: "OpenAI Organization ID",
+ placeholder: "[OPTIONAL] my-unique-org"
+ },
+ {
+ key: "api_key",
+ label: "OpenAI API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.Vertex_AI]: [
+ {
+ key: "vertex_project",
+ label: "Vertex Project",
+ placeholder: "adroit-cadet-1234..",
+ required: true
+ },
+ {
+ key: "vertex_location",
+ label: "Vertex Location",
+ placeholder: "us-east-1",
+ required: true
+ },
+ {
+ key: "vertex_credentials",
+ label: "Vertex Credentials",
+ required: true,
+ type: "upload"
+ }
+ ],
+ [Providers.AssemblyAI]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ type: "select",
+ required: true,
+ options: [
+ "https://api.assemblyai.com",
+ "https://api.eu.assemblyai.com"
+ ]
+ },
+ {
+ key: "api_key",
+ label: "AssemblyAI API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.Azure]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ placeholder: "https://...",
+ required: true
+ },
+ {
+ key: "api_version",
+ label: "API Version",
+ placeholder: "2023-07-01-preview",
+ tooltip: "By default litellm will use the latest version. If you want to use a different version, you can specify it here"
+ },
+ {
+ key: "base_model",
+ label: "Base Model",
+ placeholder: "azure/gpt-3.5-turbo"
+ },
+ {
+ key: "api_key",
+ label: "Azure API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.Azure_AI_Studio]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ placeholder: "https://...",
+ required: true
+ },
+ {
+ key: "api_key",
+ label: "Azure API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.OpenAI_Compatible]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ placeholder: "https://...",
+ required: true
+ },
+ {
+ key: "api_key",
+ label: "OpenAI API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.OpenAI_Text_Compatible]: [
+ {
+ key: "api_base",
+ label: "API Base",
+ placeholder: "https://...",
+ required: true
+ },
+ {
+ key: "api_key",
+ label: "OpenAI API Key",
+ type: "password",
+ required: true
+ }
+ ],
+ [Providers.Bedrock]: [
+ {
+ key: "aws_access_key_id",
+ label: "AWS Access Key ID",
+ required: true,
+ tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
+ },
+ {
+ key: "aws_secret_access_key",
+ label: "AWS Secret Access Key",
+ required: true,
+ tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
+ },
+ {
+ key: "aws_region_name",
+ label: "AWS Region Name",
+ placeholder: "us-east-1",
+ required: true,
+ tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
+ }
+ ],
+ [Providers.Ollama]: [], // No specific fields needed
+ [Providers.Anthropic]: [{
+ key: "api_key",
+ label: "API Key",
+ placeholder: "sk-",
+ type: "password",
+ required: true
+ }],
+ [Providers.Google_AI_Studio]: [{
+ key: "api_key",
+ label: "API Key",
+ placeholder: "aig-",
+ type: "password",
+ required: true
+ }],
+ [Providers.Groq]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.MistralAI]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Deepseek]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Cohere]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Databricks]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.xAI]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Cerebras]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Sambanova]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Perplexity]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.TogetherAI]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.Openrouter]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }],
+ [Providers.FireworksAI]: [{
+ key: "api_key",
+ label: "API Key",
+ type: "password",
+ required: true
+ }]
+};
+
const ProviderSpecificFields: React.FC = ({
selectedProvider,
uploadProps
}) => {
- console.log(`Selected provider: ${selectedProvider}`);
- console.log(`type of selectedProvider: ${typeof selectedProvider}`);
- // cast selectedProvider to Providers
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
- console.log(`selectedProviderEnum: ${selectedProviderEnum}`);
- console.log(`type of selectedProviderEnum: ${typeof selectedProviderEnum}`);
+
+ // Simply use the fields as defined in PROVIDER_CREDENTIAL_FIELDS
+ const allFields = React.useMemo(() => {
+ return PROVIDER_CREDENTIAL_FIELDS[selectedProviderEnum] || [];
+ }, [selectedProviderEnum]);
+
return (
<>
- {selectedProviderEnum === Providers.OpenAI || selectedProviderEnum === Providers.OpenAI_Text && (
- <>
+ {allFields.map((field) => (
+
-
-
-
-
-
-
- >
- )}
-
- {selectedProviderEnum === Providers.Vertex_AI && (
- <>
-
-
+ {field.type === "select" ? (
+
+ ) : field.type === "upload" ? (
+
+ }>Click to Upload
+
+ ) : (
+
+ )}
-
-
-
+ {/* Special case for Vertex Credentials help text */}
+ {field.key === "vertex_credentials" && (
+
+
+
+
+ Give litellm a gcp service account(.json file), so it
+ can make the relevant calls
+
+
+
+ )}
-
-
- }>
- Click to Upload
-
-
-
-
-
-
-
-
- Give litellm a gcp service account(.json file), so it
- can make the relevant calls
-
-
-
- >
- )}
-
- {selectedProviderEnum === Providers.AssemblyAI && (
-
-
-
- )}
-
- {(selectedProviderEnum === Providers.Azure ||
- selectedProviderEnum === Providers.Azure_AI_Studio ||
- selectedProviderEnum === Providers.OpenAI_Compatible ||
- selectedProviderEnum === Providers.OpenAI_Text_Compatible
- ) && (
-
-
-
- )}
-
- {selectedProviderEnum === Providers.Azure && (
- <>
-
-
-
-
-
-
-
-
+ {/* Special case for Azure Base Model help text */}
+ {field.key === "base_model" && (
@@ -144,54 +404,9 @@ const ProviderSpecificFields: React.FC = ({
-
- >
- )}
-
- {selectedProviderEnum === Providers.Bedrock && (
- <>
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
- {selectedProviderEnum != Providers.Bedrock &&
- selectedProviderEnum != Providers.Vertex_AI &&
- selectedProviderEnum != Providers.Ollama &&
- (
-
-
-
- )}
+ )}
+
+ ))}
>
);
};
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 46540401a94..786e94bb2a5 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -69,7 +69,6 @@ const menuItems: MenuItem[] = [
{ key: "10", page: "budgets", label: "Budgets", icon: , roles: all_admin_roles },
{ key: "11", page: "guardrails", label: "Guardrails", icon: , roles: all_admin_roles },
{ key: "18", page: "transform-request", label: "Playground", icon: , roles: all_admin_roles },
- { key: "19", page: "credentials", label: "Credentials", icon: , roles: all_admin_roles },
]
},
{
diff --git a/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx
new file mode 100644
index 00000000000..50aa4e956ec
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx
@@ -0,0 +1,119 @@
+import React, { useState } from "react";
+import {
+ Card,
+ Form,
+ Button,
+ Tooltip,
+ Typography,
+ Select as AntdSelect,
+ Input,
+ Switch,
+ Modal
+} from "antd";
+import type { UploadProps } from "antd/es/upload";
+import { Providers, providerLogoMap } from "../provider_info_helpers";
+import type { FormInstance } from "antd";
+import ProviderSpecificFields from "../add_model/provider_specific_fields";
+import { TextInput } from "@tremor/react";
+import { CredentialItem } from "../networking";
+const { Title, Link } = Typography;
+
+interface ReuseCredentialsModalProps {
+ isVisible: boolean;
+ onCancel: () => void;
+ onAddCredential: (values: any) => void;
+ existingCredential: CredentialItem | null;
+ setIsCredentialModalOpen: (isVisible: boolean) => void;
+}
+
+const ReuseCredentialsModal: React.FC = ({
+ isVisible,
+ onCancel,
+ onAddCredential,
+ existingCredential,
+ setIsCredentialModalOpen
+}) => {
+ const [form] = Form.useForm();
+
+ console.log(`existingCredential in add credentials tab: ${JSON.stringify(existingCredential)}`);
+
+ const handleSubmit = (values: any) => {
+ onAddCredential(values);
+ form.resetFields();
+ setIsCredentialModalOpen(false);
+ };
+
+ return (
+ {
+ onCancel();
+ form.resetFields();
+ }}
+ footer={null}
+ width={600}
+ >
+
+
+
+
+ {/* Display Credential Values of existingCredential, don't allow user to edit. Credential values is a dictionary */}
+ {Object.entries(existingCredential?.credential_values || {}).map(([key, value]) => (
+
+
+
+ ))}
+
+ {/* Modal Footer */}
+
+
+
+ Need Help?
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ReuseCredentialsModal;
\ No newline at end of file
diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx
index bc6a831d628..7768aa71cbb 100644
--- a/ui/litellm-dashboard/src/components/model_info_view.tsx
+++ b/ui/litellm-dashboard/src/components/model_info_view.tsx
@@ -14,13 +14,15 @@ import {
TextInput,
NumberInput,
} from "@tremor/react";
-import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
-import { modelDeleteCall, modelUpdateCall } from "./networking";
-import { Button, Form, Input, InputNumber, message, Select } from "antd";
+import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline";
+import { modelDeleteCall, modelUpdateCall, CredentialItem, credentialGetCall, credentialCreateCall } from "./networking";
+import { Button, Form, Input, InputNumber, message, Select, Modal } from "antd";
import EditModelModal from "./edit_model/edit_model_modal";
import { handleEditModelSubmit } from "./edit_model/edit_model_modal";
import { getProviderLogoAndName } from "./provider_info_helpers";
import { getDisplayModelName } from "./view_model/model_name_display";
+import AddCredentialsModal from "./model_add/add_credentials_tab";
+import ReuseCredentialsModal from "./model_add/reuse_credentials";
interface ModelInfoViewProps {
modelId: string;
@@ -48,11 +50,51 @@ export default function ModelInfoView({
const [form] = Form.useForm();
const [localModelData, setLocalModelData] = useState(modelData);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [isCredentialModalOpen, setIsCredentialModalOpen] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState(false);
+ const [existingCredential, setExistingCredential] = useState(null);
const canEditModel = userRole === "Admin";
+ const isAdmin = userRole === "Admin";
+
+ const usingExistingCredential = modelData.litellm_params?.litellm_credential_name != null && modelData.litellm_params?.litellm_credential_name != undefined;
+ console.log("usingExistingCredential, ", usingExistingCredential);
+ console.log("modelData.litellm_params.litellm_credential_name, ", modelData.litellm_params.litellm_credential_name);
+
+
+ useEffect(() => {
+ const getExistingCredential = async () => {
+ console.log("accessToken, ", accessToken);
+ if (!accessToken) return;
+ if (usingExistingCredential) return;
+ let existingCredentialResponse = await credentialGetCall(accessToken, null, modelId);
+ console.log("existingCredentialResponse, ", existingCredentialResponse);
+ setExistingCredential({
+ credential_name: existingCredentialResponse["credential_name"],
+ credential_values: existingCredentialResponse["credential_values"],
+ credential_info: existingCredentialResponse["credential_info"]
+ });
+ }
+ getExistingCredential();
+ }, [accessToken, modelId]);
+
+ const handleReuseCredential = async (values: any) => {
+ console.log("values, ", values);
+ if (!accessToken) return;
+ let credentialItem = {
+ credential_name: values.credential_name,
+ model_id: modelId,
+ credential_info: {
+ "custom_llm_provider": localModelData.litellm_params?.custom_llm_provider,
+ }
+ }
+ message.info("Storing credential..");
+ let credentialResponse = await credentialCreateCall(accessToken, credentialItem);
+ console.log("credentialResponse, ", credentialResponse);
+ message.success("Credential stored successfully");
+ }
const handleModelUpdate = async (values: any) => {
try {
@@ -143,8 +185,16 @@ export default function ModelInfoView({
Public Model Name: {getDisplayModelName(modelData)}
{modelData.model_info.id}
- {canEditModel && (
+ {isAdmin && (
+ setIsCredentialModalOpen(true)}
+ className="flex items-center"
+ >
+ Re-use Credentials
+
)}
+
+ {isCredentialModalOpen &&
+ !usingExistingCredential ? (
+ setIsCredentialModalOpen(false)}
+ onAddCredential={handleReuseCredential}
+ existingCredential={existingCredential}
+ setIsCredentialModalOpen={setIsCredentialModalOpen}
+ />
+ ): (
+ setIsCredentialModalOpen(false)}
+ title="Using Existing Credential"
+ >
+ {modelData.litellm_params.litellm_credential_name}
+
+ )}
);
}
\ No newline at end of file
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 19589400a0e..0c1ba991aca 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -2652,6 +2652,42 @@ export const credentialListCall = async (
}
};
+export const credentialGetCall = async (accessToken: String, credentialName: String | null, modelId: String | null) => {
+ try {
+ let url = proxyBaseUrl ? `${proxyBaseUrl}/credentials` : `/credentials`;
+
+ if (credentialName) {
+ url += `/by_name/${credentialName}`;
+ } else if (modelId) {
+ url += `/by_model/${modelId}`;
+ }
+
+ console.log("in credentialListCall");
+
+ 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();
+ console.log("/credentials API Response:", data);
+ return data;
+ // Handle success - you might want to update some state or UI based on the created key
+ } catch (error) {
+ console.error("Failed to create key:", error);
+ throw error;
+ }
+};
+
export const credentialDeleteCall = async (accessToken: String, credentialName: String) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`;
@@ -2698,7 +2734,7 @@ export const credentialUpdateCall = async (
const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`;
const response = await fetch(url, {
- method: "PUT",
+ method: "PATCH",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",