From 3f3efea3016bb52d93601d32f326fd02e8ac4313 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 11 Sep 2025 15:12:21 -0700 Subject: [PATCH 001/189] test(test_gemini.py): add additional testing for additionalproperties case --- litellm/types/llms/vertex_ai.py | 1 - tests/llm_translation/test_gemini.py | 79 ++++++++++++++----- .../test_amazing_vertex_completion.py | 50 +++++++++--- 3 files changed, 96 insertions(+), 34 deletions(-) diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index 2687b79f727..f17a284ddfc 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -113,7 +113,6 @@ class Schema(TypedDict, total=False): pattern: str example: Any anyOf: List["Schema"] - additionalProperties: Any class FunctionDeclaration(TypedDict, total=False): diff --git a/tests/llm_translation/test_gemini.py b/tests/llm_translation/test_gemini.py index 47e3aaa8143..122429a87a5 100644 --- a/tests/llm_translation/test_gemini.py +++ b/tests/llm_translation/test_gemini.py @@ -269,7 +269,11 @@ def test_gemini_image_generation(): assert len(response.choices[0].message.images) > 0 assert response.choices[0].message.images[0]["image_url"] is not None assert response.choices[0].message.images[0]["image_url"]["url"] is not None - assert response.choices[0].message.images[0]["image_url"]["url"].startswith("data:image/png;base64,") + assert ( + response.choices[0] + .message.images[0]["image_url"]["url"] + .startswith("data:image/png;base64,") + ) def test_gemini_thinking(): @@ -661,7 +665,8 @@ def test_system_message_with_no_user_message(): assert response is not None assert response.choices[0].message.content is not None - + + def get_current_weather(location, unit="fahrenheit"): """Get the current weather in a given location""" if "tokyo" in location.lower(): @@ -778,9 +783,9 @@ def test_gemini_reasoning_effort_minimal(): # Test with different Gemini models to verify model-specific mapping test_cases = [ - ("gemini/gemini-2.5-flash", 1), # Flash: minimum 1 token - ("gemini/gemini-2.5-pro", 128), # Pro: minimum 128 tokens - ("gemini/gemini-2.5-flash-lite", 512), # Flash-Lite: minimum 512 tokens + ("gemini/gemini-2.5-flash", 1), # Flash: minimum 1 token + ("gemini/gemini-2.5-pro", 128), # Pro: minimum 128 tokens + ("gemini/gemini-2.5-flash-lite", 512), # Flash-Lite: minimum 512 tokens ] for model, expected_min_budget in test_cases: @@ -793,24 +798,32 @@ def test_gemini_reasoning_effort_minimal(): "reasoning_effort": "minimal", }, ) - + # Verify that the thinking config is set correctly request_body = raw_request["raw_request_body"] - assert "generationConfig" in request_body, f"Model {model} should have generationConfig" - + assert ( + "generationConfig" in request_body + ), f"Model {model} should have generationConfig" + generation_config = request_body["generationConfig"] - assert "thinkingConfig" in generation_config, f"Model {model} should have thinkingConfig" - + assert ( + "thinkingConfig" in generation_config + ), f"Model {model} should have thinkingConfig" + thinking_config = generation_config["thinkingConfig"] - assert "thinkingBudget" in thinking_config, f"Model {model} should have thinkingBudget" - + assert ( + "thinkingBudget" in thinking_config + ), f"Model {model} should have thinkingBudget" + actual_budget = thinking_config["thinkingBudget"] - assert actual_budget == expected_min_budget, \ - f"Model {model} should map 'minimal' to {expected_min_budget} tokens, got {actual_budget}" - + assert ( + actual_budget == expected_min_budget + ), f"Model {model} should map 'minimal' to {expected_min_budget} tokens, got {actual_budget}" + # Verify that includeThoughts is True for minimal reasoning effort - assert thinking_config.get("includeThoughts", True), \ - f"Model {model} should have includeThoughts=True for minimal reasoning effort" + assert thinking_config.get( + "includeThoughts", True + ), f"Model {model} should have includeThoughts=True for minimal reasoning effort" # Test with unknown model (should use generic fallback) try: @@ -822,15 +835,41 @@ def test_gemini_reasoning_effort_minimal(): "reasoning_effort": "minimal", }, ) - + request_body = raw_request["raw_request_body"] generation_config = request_body["generationConfig"] thinking_config = generation_config["thinkingConfig"] # Should use generic fallback (128 tokens) - assert thinking_config["thinkingBudget"] == 128, \ - "Unknown model should use generic fallback of 128 tokens" + assert ( + thinking_config["thinkingBudget"] == 128 + ), "Unknown model should use generic fallback of 128 tokens" except Exception as e: # If return_raw_request doesn't work for unknown models, that's okay # The important part is that our known models work correctly print(f"Note: Unknown model test skipped due to: {e}") pass + + +def test_gemini_additional_properties_bug(): + # Simple tool with additionalProperties (simulating the TypedDict issue) + tools = [ + { + "type": "function", + "function": { + "name": "test_tool", + "description": "Test tool", + "parameters": { + "type": "object", + "properties": {"param1": {"type": "string"}}, + # This causes the error - any non-False value + "additionalProperties": True, # Could also be None, {}, etc. + }, + }, + } + ] + + messages = [{"role": "user", "content": "Test message"}] + + response = litellm.completion( + model="gemini/gemini-2.5-flash", messages=messages, tools=tools + ) diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index a27fe738c7f..d20f54bdd34 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -397,7 +397,7 @@ async def test_async_vertexai_response(): | litellm.vertex_text_models | litellm.vertex_code_text_models ) - + test_models = random.sample(list(test_models), 1) test_models += list(litellm.vertex_language_models) # always test gemini-pro for model in test_models: @@ -504,7 +504,6 @@ async def test_async_vertexai_streaming_response(): pytest.fail(f"An exception occurred: {e}") - @pytest.mark.parametrize("load_pdf", [False]) # True, @pytest.mark.flaky(retries=3, delay=1) def test_completion_function_plus_pdf(load_pdf): @@ -547,6 +546,7 @@ def test_completion_function_plus_pdf(load_pdf): except Exception as e: pytest.fail("Got={}".format(str(e))) + def encode_image(image_path): import base64 @@ -910,7 +910,10 @@ async def test_partner_models_httpx(model, region, sync_mode): [ ("vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas", "us-east5"), ("vertex_ai/qwen/qwen3-coder-480b-a35b-instruct-maas", "us-south1"), - ("vertex_ai/mistral-large-2411", "us-central1"), # critical - we had this issue: https://github.com/BerriAI/litellm/issues/13888 + ( + "vertex_ai/mistral-large-2411", + "us-central1", + ), # critical - we had this issue: https://github.com/BerriAI/litellm/issues/13888 ("vertex_ai/openai/gpt-oss-20b-maas", "us-central1"), ], ) @@ -3827,7 +3830,7 @@ def test_vertex_ai_gemini_audio_ogg(): @pytest.mark.asyncio async def test_vertex_ai_deepseek(): """Test that deepseek models use the correct v1 API endpoint instead of v1beta1.""" - #load_vertex_ai_credentials() + # load_vertex_ai_credentials() litellm._turn_on_debug() from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler @@ -3840,21 +3843,17 @@ async def test_vertex_ai_deepseek(): { "message": { "role": "assistant", - "content": "Hello! How can I help you today?" + "content": "Hello! How can I help you today?", }, "index": 0, - "finish_reason": "stop" + "finish_reason": "stop", } ], - "usage": { - "prompt_tokens": 10, - "completion_tokens": 20, - "total_tokens": 30 - }, - "model": "deepseek-ai/deepseek-r1-0528-maas" + "usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}, + "model": "deepseek-ai/deepseek-r1-0528-maas", } mock_response.status_code = 200 - + with patch.object(client, "post", return_value=mock_response) as mock_post: response = await acompletion( model="vertex_ai/deepseek-ai/deepseek-r1-0528-maas", @@ -3900,3 +3899,28 @@ def test_gemini_grounding_on_streaming(): vertex_ai_grounding_metadata_shows_up = True print(chunk) assert vertex_ai_grounding_metadata_shows_up + + +def test_gemini_additional_properties_bug(): + # Simple tool with additionalProperties (simulating the TypedDict issue) + tools = [ + { + "type": "function", + "function": { + "name": "test_tool", + "description": "Test tool", + "parameters": { + "type": "object", + "properties": {"param1": {"type": "string"}}, + # This causes the error - any non-False value + "additionalProperties": True, # Could also be None, {}, etc. + }, + }, + } + ] + + messages = [{"role": "user", "content": "Test message"}] + + response = litellm.completion( + model="gemini/gemini-2.5-flash", messages=messages, tools=tools + ) From 84a7329dba2838d6de6e9c1e6a4a7c03dfb9076d Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 11 Sep 2025 16:04:06 -0700 Subject: [PATCH 002/189] fix(secret_managers/get_azure_Ad_token_providers.py): infer credential type from env var don't default to ClientSecretCredential unless present in env var --- litellm/llms/azure/common_utils.py | 4 ++- litellm/proxy/_new_secret_config.yaml | 8 +++++ .../get_azure_ad_token_provider.py | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 09b1888e04d..b36375e4168 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -561,7 +561,9 @@ class BaseAzureLLM(BaseOpenAILLM): "Using Azure AD token provider based on Service Principal with Secret workflow for Azure Auth" ) try: - azure_ad_token_provider = get_azure_ad_token_provider(azure_scope=scope) + azure_ad_token_provider = get_azure_ad_token_provider( + azure_scope=scope, + ) except ValueError: verbose_logger.debug("Azure AD Token Provider could not be used.") if api_version is None: diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index c785dd05c40..be2a8303864 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -7,3 +7,11 @@ model_list: - model_name: wildcard_models/* litellm_params: model: openai/* + - model_name: gpt-4o + litellm_params: + model: azure/gpt-4o + api_base: https://cog-cuda-atg-sage-eastus2.openai.azure.com + api_version: 2025-04-01-preview + +litellm_settings: + enable_azure_ad_token_refresh: true \ No newline at end of file diff --git a/litellm/secret_managers/get_azure_ad_token_provider.py b/litellm/secret_managers/get_azure_ad_token_provider.py index e73c8f8d7a2..184d959b964 100644 --- a/litellm/secret_managers/get_azure_ad_token_provider.py +++ b/litellm/secret_managers/get_azure_ad_token_provider.py @@ -1,11 +1,36 @@ import os from typing import Any, Callable, Optional, Union +from litellm._logging import verbose_logger from litellm.types.secret_managers.get_azure_ad_token_provider import ( AzureCredentialType, ) +def infer_credential_type_from_environment() -> AzureCredentialType: + if ( + os.environ.get("AZURE_CLIENT_ID") + and os.environ.get("AZURE_CLIENT_SECRET") + and os.environ.get("AZURE_TENANT_ID") + ): + return AzureCredentialType.ClientSecretCredential + elif os.environ.get("AZURE_CLIENT_ID"): + return AzureCredentialType.ManagedIdentityCredential + elif ( + os.environ.get("AZURE_CLIENT_ID") + and os.environ.get("AZURE_TENANT_ID") + and os.environ.get("AZURE_CERTIFICATE_PATH") + and os.environ.get("AZURE_CERTIFICATE_PASSWORD") + ): + return AzureCredentialType.CertificateCredential + elif os.environ.get("AZURE_CERTIFICATE_PASSWORD"): + return AzureCredentialType.CertificateCredential + elif os.environ.get("AZURE_CERTIFICATE_PATH"): + return AzureCredentialType.CertificateCredential + else: + return AzureCredentialType.DefaultAzureCredential + + def get_azure_ad_token_provider( azure_scope: Optional[str] = None, azure_credential: Optional[AzureCredentialType] = None, @@ -42,9 +67,14 @@ def get_azure_ad_token_provider( ) cred: str = ( - azure_credential.value if azure_credential else None - or os.environ.get("AZURE_CREDENTIAL", AzureCredentialType.ClientSecretCredential) - or AzureCredentialType.ClientSecretCredential + azure_credential.value + if azure_credential + else None + or os.environ.get("AZURE_CREDENTIAL") + or infer_credential_type_from_environment() + ) + verbose_logger.info( + f"For Azure AD Token Provider, choosing credential type: {cred}" ) credential: Optional[ Union[ From d0732f55b3954da5a6853447a99fb8ae5a38edbe Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 11 Sep 2025 16:07:32 -0700 Subject: [PATCH 003/189] test(test_get_azure_ad_token_provider.py): add unit test to ensure default azure credentials used in the right context --- .../test_get_azure_ad_token_provider.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_litellm/secret_managers/test_get_azure_ad_token_provider.py b/tests/test_litellm/secret_managers/test_get_azure_ad_token_provider.py index 85e55a5c30d..f02f59cccc0 100644 --- a/tests/test_litellm/secret_managers/test_get_azure_ad_token_provider.py +++ b/tests/test_litellm/secret_managers/test_get_azure_ad_token_provider.py @@ -214,3 +214,32 @@ class TestGetAzureAdTokenProvider: # Test that the returned callable works token = result() assert token == "mock-certificate-token" + + @patch.dict(os.environ, {}, clear=True) # Clear all environment variables + @patch("azure.identity.get_bearer_token_provider") + @patch("azure.identity.DefaultAzureCredential") + def test_get_azure_ad_token_provider_defaults_to_default_azure_credential( + self, mock_default_azure_credential, mock_get_bearer_token_provider + ): + """Test get_azure_ad_token_provider defaults to DefaultAzureCredential when no credentials are present.""" + # Mock the Azure identity credential instance + mock_credential_instance = MagicMock() + mock_default_azure_credential.return_value = mock_credential_instance + + # Mock the bearer token provider + mock_token_provider = MagicMock(return_value="mock-default-token") + mock_get_bearer_token_provider.return_value = mock_token_provider + + # Call the function + result = get_azure_ad_token_provider() + + # Assertions + assert callable(result) + mock_default_azure_credential.assert_called_once_with() + mock_get_bearer_token_provider.assert_called_once_with( + mock_credential_instance, "https://cognitiveservices.azure.com/.default" + ) + + # Test that the returned callable works + token = result() + assert token == "mock-default-token" From 54e71bd0775adeab4df7015e334dd7ac3cbc551b Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 11 Sep 2025 16:10:13 -0700 Subject: [PATCH 004/189] fix(common_utils.py): add helpful message --- litellm/llms/azure/common_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index b36375e4168..7c744298fbd 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -365,6 +365,11 @@ def get_azure_ad_token( azure_ad_token_provider = get_azure_ad_token_provider(azure_scope=scope) except ValueError: verbose_logger.debug("Azure AD Token Provider could not be used.") + except Exception as e: + verbose_logger.error( + f"Error calling Azure AD token provider: {str(e)}. Follow docs - https://docs.litellm.ai/docs/providers/azure/#azure-ad-token-refresh---defaultazurecredential" + ) + raise e ######################################################### # If litellm.enable_azure_ad_token_refresh is True and no other token provider is available, From 5e03ef73820f0ef64d472bd0f36186f5b91bf1c7 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Oct 2025 14:32:15 -0700 Subject: [PATCH 005/189] fixes bloated key alias network calls with lean endpoint --- .../key_management_endpoints.py | 75 ++++++++++++++++++- .../test_key_generate_prisma.py | 54 ++++++++++++- .../key_team_helpers/filter_helpers.ts | 42 ++--------- .../src/components/networking.tsx | 35 +++++++++ 4 files changed, 168 insertions(+), 38 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 007c0164be4..54e409bba79 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -15,7 +15,7 @@ import json import secrets import traceback from datetime import datetime, timedelta, timezone -from typing import List, Literal, Optional, Tuple, cast +from typing import List, Literal, Optional, Tuple, cast, Set import fastapi from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status @@ -2614,6 +2614,79 @@ async def list_keys( code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) +@router.get( + "/key/aliases", + tags=["key management"], + dependencies=[Depends(user_api_key_auth)], +) +@management_endpoint_wrapper +async def key_aliases() -> Dict[str, List[str]]: + """ + Lists all key aliases + + Returns: + { + "aliases": List[str] + } + """ + try: + from litellm.proxy.proxy_server import prisma_client + + verbose_proxy_logger.debug("Entering key_aliases function") + + if prisma_client is None: + verbose_proxy_logger.error("Database not connected") + raise Exception("Database not connected") + + where: Dict[str, Any] = {} + try: + where.update(_get_condition_to_filter_out_ui_session_tokens()) + except NameError: + # Helper may not exist in some builds; ignore if missing + pass + + rows = await prisma_client.db.litellm_verificationtoken.find_many( + where=where, + order=[{"key_alias": "asc"}], + ) + + seen = set() + aliases: List[str] = [] + for row in rows: + alias = getattr(row, "key_alias", None) + if alias is None and isinstance(row, dict): + alias = row.get("key_alias") + + if not alias: + continue + + alias_str = str(alias).strip() + if alias_str and alias_str not in seen: + seen.add(alias_str) + aliases.append(alias_str) + + verbose_proxy_logger.debug(f"Returning {len(aliases)} key aliases") + + return {"aliases": aliases} + + except Exception as e: + verbose_proxy_logger.exception(f"Error in key_aliases: {e}") + if isinstance(e, HTTPException): + raise ProxyException( + message=getattr(e, "detail", f"error({str(e)})"), + type=ProxyErrorTypes.internal_server_error, + param=getattr(e, "param", "None"), + code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), + ) + elif isinstance(e, ProxyException): + raise e + raise ProxyException( + message="Authentication Error, " + str(e), + type=ProxyErrorTypes.internal_server_error, + param=getattr(e, "param", "None"), + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + def _validate_sort_params( sort_by: Optional[str], sort_order: str diff --git a/tests/proxy_unit_tests/test_key_generate_prisma.py b/tests/proxy_unit_tests/test_key_generate_prisma.py index 6eb4efa7d63..a8df57283ca 100644 --- a/tests/proxy_unit_tests/test_key_generate_prisma.py +++ b/tests/proxy_unit_tests/test_key_generate_prisma.py @@ -60,7 +60,7 @@ from litellm.proxy.management_endpoints.key_management_endpoints import ( info_key_fn, list_keys, regenerate_key_fn, - update_key_fn, + update_key_fn, key_aliases, ) from litellm.proxy.management_endpoints.team_endpoints import ( new_team, @@ -3528,6 +3528,58 @@ async def test_list_keys(prisma_client): assert _key in response["keys"] +@pytest.mark.asyncio +async def test_key_aliases(prisma_client): + """ + Test the key_aliases function: + - Returns a list + - Includes alias from a newly created key + - Aliases are unique and sorted + """ + import asyncio + import uuid + import litellm + from litellm.proxy._types import LitellmUserRoles + + # Wire up test prisma client + setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) + setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") + await litellm.proxy.proxy_server.prisma_client.connect() + + # Basic call + response = await key_aliases() + assert "aliases" in response + assert isinstance(response["aliases"], list) + + # Create a new user (and key) with a unique alias + unique_id = str(uuid.uuid4()) + test_alias = f"key-aliases-test-{unique_id}" + test_user_id = f"key-aliases-user-{unique_id}" + + await new_user( + data=NewUserRequest( + user_id=test_user_id, + user_role=LitellmUserRoles.INTERNAL_USER, + key_alias=test_alias, + ), + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), + ) + + # Allow async DB writes to settle + await asyncio.sleep(2) + + # Call again and validate + response_after = await key_aliases() + aliases = response_after["aliases"] + + # Contains the new alias + assert test_alias in aliases + + # Unique & sorted (endpoint dedupes and orders ascending) + assert len(aliases) == len(set(aliases)) + assert aliases == sorted(aliases) + + @pytest.mark.asyncio async def test_auth_vertex_ai_route(prisma_client): """ diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index 887815596db..a46666cddc9 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -1,9 +1,9 @@ -import { keyListCall, teamListCall, organizationListCall } from "../networking"; +import { teamListCall, organizationListCall, keyAliasesCall } from "../networking" import { Team } from "./key_list"; import { Organization } from "../networking"; /** - * Fetches all key aliases across all pages + * Fetches all key aliases via the dedicated /key/aliases endpoint * @param accessToken The access token for API authentication * @returns Array of all unique key aliases */ @@ -13,46 +13,16 @@ export const fetchAllKeyAliases = async ( if (!accessToken) return []; try { - // Fetch all pages of keys to extract aliases - let allAliases: string[] = []; - let currentPage = 1; - let hasMorePages = true; - - while (hasMorePages) { - const response = await keyListCall( - accessToken, - null, // organization_id - "", // team_id - null, // selectedKeyAlias - null, // user_id - null, // key_hash - currentPage, - 100 // larger page size to reduce number of requests - ); - - // Extract aliases from this page - const pageAliases = response.keys - .map((key: any) => key.key_alias) - .filter(Boolean) as string[]; - - allAliases = [...allAliases, ...pageAliases]; - - // Check if there are more pages - if (currentPage < response.total_pages) { - currentPage++; - } else { - hasMorePages = false; - } - } - - // Remove duplicates - return Array.from(new Set(allAliases)); + const { aliases } = await keyAliasesCall(accessToken as unknown as String); + // Defensive dedupe & null-guard + return Array.from(new Set((aliases || []).filter(Boolean))); } catch (error) { console.error("Error fetching all key aliases:", error); return []; } }; + /** * Fetches all teams across all pages * @param accessToken The access token for API authentication diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 6b8c7671a10..f674df444a5 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -3305,6 +3305,41 @@ export const keyListCall = async ( } }; +export const keyAliasesCall = async ( + accessToken: String +): Promise<{ aliases: string[] }> => { + /** + * Get all key aliases from proxy + */ + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/key/aliases` : `/key/aliases`; + console.log("in keyAliasesCall"); + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("/key/aliases API Response:", data); + return data; // { aliases: string[] } + } catch (error) { + console.error("Failed to fetch key aliases:", error); + throw error; + } +}; + + export const spendUsersCall = async (accessToken: String, userID: String) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/spend/users` : `/spend/users`; From 6ba077593f1601bcadb7f15aa729002d38a49ee9 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Oct 2025 14:36:19 -0700 Subject: [PATCH 006/189] Update test_key_generate_prisma.py --- tests/proxy_unit_tests/test_key_generate_prisma.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/proxy_unit_tests/test_key_generate_prisma.py b/tests/proxy_unit_tests/test_key_generate_prisma.py index a8df57283ca..1968fde8436 100644 --- a/tests/proxy_unit_tests/test_key_generate_prisma.py +++ b/tests/proxy_unit_tests/test_key_generate_prisma.py @@ -60,7 +60,8 @@ from litellm.proxy.management_endpoints.key_management_endpoints import ( info_key_fn, list_keys, regenerate_key_fn, - update_key_fn, key_aliases, + update_key_fn, + key_aliases, ) from litellm.proxy.management_endpoints.team_endpoints import ( new_team, @@ -151,7 +152,6 @@ def prisma_client(): @pytest.mark.flaky(retries=6, delay=1) async def test_new_user_response(prisma_client): try: - print("prisma client=", prisma_client) setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) @@ -424,7 +424,6 @@ async def test_call_with_valid_model_using_all_models(prisma_client): setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") try: - await litellm.proxy.proxy_server.prisma_client.connect() team_request = NewTeamRequest( @@ -1786,7 +1785,6 @@ async def test_call_with_key_over_model_budget( litellm.callbacks.append(model_budget_limiter) try: - # set budget for chatgpt-v-3 to 0.000001, expect the next request to fail model_max_budget = { "gpt-4o-mini": { From 1ce21d1f58df807fcb11660f1ce6466545d153b4 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Oct 2025 16:02:15 -0700 Subject: [PATCH 007/189] Update key_management_endpoints.py --- litellm/proxy/management_endpoints/key_management_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 54e409bba79..cf101543011 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -15,7 +15,7 @@ import json import secrets import traceback from datetime import datetime, timedelta, timezone -from typing import List, Literal, Optional, Tuple, cast, Set +from typing import List, Literal, Optional, Tuple, cast import fastapi from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status From 9a1c0145dab0a12b45a98526b4dbbbc4c592bee0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Oct 2025 09:20:17 +0900 Subject: [PATCH 008/189] feat: add Global Cross-Region Inference --- litellm/llms/bedrock/common_utils.py | 2 +- ...odel_prices_and_context_window_backup.json | 78 +++++++++++++------ model_prices_and_context_window.json | 78 +++++++++++++------ 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 241359d937e..bd371414db5 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -440,7 +440,7 @@ class BedrockModelInfo(BaseLLMModelInfo): """ Abbreviations of regions AWS Bedrock supports for cross region inference """ - return ["us", "eu", "apac", "jp"] + return ["global", "us", "eu", "apac", "jp"] @staticmethod def get_bedrock_route( diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 7ed04dc79c9..72ff60883b0 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -7838,19 +7838,19 @@ "tool_use_system_prompt_tokens": 159 }, "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -11820,6 +11820,36 @@ "video" ] }, + "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, "gpt-3.5-turbo": { "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", @@ -14219,19 +14249,19 @@ "output_cost_per_token": 1.8e-08 }, "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -19848,19 +19878,19 @@ "tool_use_system_prompt_tokens": 159 }, "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 33e-07, + "input_cost_per_token": 33e-06, + "input_cost_per_token_above_200k_tokens": 66e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 66e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 7ed04dc79c9..72ff60883b0 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -7838,19 +7838,19 @@ "tool_use_system_prompt_tokens": 159 }, "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -11820,6 +11820,36 @@ "video" ] }, + "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, "gpt-3.5-turbo": { "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", @@ -14219,19 +14249,19 @@ "output_cost_per_token": 1.8e-08 }, "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -19848,19 +19878,19 @@ "tool_use_system_prompt_tokens": 159 }, "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - "cache_creation_input_token_cost": 3.75e-06, - "cache_read_input_token_cost": 3e-07, - "input_cost_per_token": 3e-06, - "input_cost_per_token_above_200k_tokens": 6e-06, - "output_cost_per_token_above_200k_tokens": 2.25e-05, - "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, - "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 33e-07, + "input_cost_per_token": 33e-06, + "input_cost_per_token_above_200k_tokens": 66e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 66e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 64000, "max_tokens": 200000, "mode": "chat", - "output_cost_per_token": 1.5e-05, + "output_cost_per_token": 1.65e-05, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, From ba0dcfc001587f823dd84e38a2bf54500ac5e2f6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 5 Oct 2025 09:25:39 +0900 Subject: [PATCH 009/189] feat: add Global Cross-Region Sonnet 4 --- ...odel_prices_and_context_window_backup.json | 30 +++++++++++++++++++ model_prices_and_context_window.json | 30 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 72ff60883b0..4328149b655 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -11850,6 +11850,36 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 346 }, + "global.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "gpt-3.5-turbo": { "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 72ff60883b0..4328149b655 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -11850,6 +11850,36 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 346 }, + "global.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "gpt-3.5-turbo": { "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", From 5197268a58afc4eb1e48ed06fce30b6c52f81047 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 4 Oct 2025 18:19:48 -0700 Subject: [PATCH 010/189] added and ran prettier autoformatter --- ui/litellm-dashboard/.prettierignore | 11 + ui/litellm-dashboard/.prettierrc | 7 + ui/litellm-dashboard/.prettierrc.json | 7 - ui/litellm-dashboard/next.config.mjs | 10 +- ui/litellm-dashboard/package-lock.json | 1 + ui/litellm-dashboard/package.json | 4 +- ui/litellm-dashboard/src/app/globals.css | 7 +- .../src/app/model_hub/page.tsx | 4 +- .../src/app/model_hub_table/page.tsx | 6 +- .../src/app/onboarding/page.tsx | 68 +- ui/litellm-dashboard/src/app/page.tsx | 224 +-- ui/litellm-dashboard/src/components/SCIM.tsx | 70 +- .../src/components/SSOModals.tsx | 166 +- .../src/components/SSOSettings.tsx | 170 +- .../src/components/TeamSSOSettings.tsx | 108 +- .../src/components/UIAccessControlForm.tsx | 58 +- .../UIAccessControlForm.unit.test.tsx | 126 +- .../src/components/activity_metrics.tsx | 211 +-- .../src/components/add_fallbacks.tsx | 264 ++-- .../add_model/add_auto_router_tab.tsx | 113 +- .../components/add_model/add_model_modes.tsx | 8 +- .../components/add_model/add_model_tab.tsx | 488 +++--- .../add_model/advanced_settings.tsx | 85 +- .../add_model/cache_control_settings.tsx | 57 +- .../conditional_public_model_name.tsx | 127 +- .../handle_add_auto_router_submit.tsx | 19 +- .../add_model/handle_add_model_submit.tsx | 339 ++-- .../add_model/litellm_model_name.tsx | 125 +- .../add_model/model_connection_test.tsx | 260 ++-- .../add_model/provider_specific_fields.tsx | 535 ++++--- .../add_model/router_config_builder.tsx | 93 +- .../src/components/add_pass_through.tsx | 157 +- .../src/components/admins.tsx | 334 ++-- .../components/alerting/alerting_settings.tsx | 25 +- .../src/components/alerting/dynamic_form.tsx | 34 +- .../src/components/all_keys_table.tsx | 292 ++-- .../src/components/api_ref.tsx | 84 +- .../src/components/atoms/Tooltip.tsx | 40 +- .../src/components/atoms/index.ts | 2 +- .../src/components/budgets/budget_modal.tsx | 57 +- .../src/components/budgets/budget_panel.tsx | 35 +- .../components/budgets/budget_settings.tsx | 51 +- .../components/budgets/edit_budget_modal.tsx | 50 +- .../components/bulk_create_users_button.tsx | 376 ++--- .../src/components/bulk_edit_user.tsx | 233 ++- .../src/components/cache_dashboard.tsx | 479 +++--- .../src/components/cache_health.tsx | 180 +-- .../src/components/callback_info_helpers.tsx | 159 +- .../components/chat_ui/ChatImageRenderer.tsx | 14 +- .../components/chat_ui/ChatImageUpload.tsx | 6 +- .../src/components/chat_ui/ChatImageUtils.tsx | 28 +- .../src/components/chat_ui/ChatUI.tsx | 1373 +++++++++-------- .../src/components/chat_ui/CodeSnippets.tsx | 206 +-- .../components/chat_ui/EndpointSelector.tsx | 18 +- .../src/components/chat_ui/EndpointUtils.tsx | 17 +- .../components/chat_ui/MCPEventsDisplay.tsx | 60 +- .../components/chat_ui/ReasoningContent.tsx | 24 +- .../components/chat_ui/ResponseMetrics.tsx | 26 +- .../chat_ui/ResponsesImageRenderer.tsx | 14 +- .../chat_ui/ResponsesImageUpload.tsx | 6 +- .../chat_ui/ResponsesImageUtils.tsx | 32 +- .../components/chat_ui/SessionManagement.tsx | 53 +- .../chat_ui/llm_calls/anthropic_messages.tsx | 47 +- .../chat_ui/llm_calls/chat_completion.tsx | 259 ++-- .../chat_ui/llm_calls/fetch_mcp_tools.tsx | 8 +- .../chat_ui/llm_calls/fetch_models.tsx | 4 +- .../chat_ui/llm_calls/image_edits.tsx | 40 +- .../chat_ui/llm_calls/image_generation.tsx | 19 +- .../chat_ui/llm_calls/process_stream.tsx | 19 +- .../chat_ui/llm_calls/responses_api.tsx | 110 +- .../chat_ui/mode_endpoint_mapping.tsx | 74 +- .../src/components/chat_ui/types.ts | 2 +- .../src/components/cloudzero_export_modal.tsx | 120 +- .../common_components/AutoRotationView.tsx | 122 +- .../KeyLifecycleSettings.tsx | 15 +- .../common_components/ModelAliasManager.tsx | 102 +- .../common_components/ModelSelector.tsx | 27 +- .../PremiumLoggingSettings.tsx | 13 +- .../common_components/PremiumMCPSelector.tsx | 21 +- .../PremiumVectorStoreSelector.tsx | 21 +- .../RateLimitTypeFormItem.tsx | 60 +- .../components/common_components/all_view.tsx | 74 +- .../budget_duration_dropdown.tsx | 12 +- .../common_components/chartUtils.tsx | 37 +- .../check_openapi_schema.tsx | 140 +- .../common_components/default_org.tsx | 8 +- .../common_components/fetch_teams.tsx | 28 +- .../common_components/team_dropdown.tsx | 15 +- .../common_components/user_form.tsx | 48 +- .../common_components/user_search_modal.tsx | 101 +- .../src/components/constants.tsx | 6 +- .../src/components/create_user_button.tsx | 190 +-- .../src/components/dashboard_default_team.tsx | 17 +- .../src/components/delete_model_button.tsx | 95 +- .../edit_auto_router_modal.tsx | 45 +- .../edit_model/edit_model_modal.tsx | 55 +- .../src/components/edit_user.tsx | 156 +- .../email_events/email_event_settings.tsx | 38 +- .../src/components/email_events/index.ts | 4 +- .../src/components/email_events/types.ts | 2 +- .../src/components/email_settings.tsx | 254 ++- .../src/components/enter_proxy_url.tsx | 35 +- .../src/components/entity_usage.tsx | 298 ++-- .../src/components/general_settings.tsx | 313 ++-- .../components/generic_key_value_manager.tsx | 369 +++-- .../src/components/guardrails.tsx | 124 +- .../guardrails/GuardrailSelector.tsx | 28 +- .../src/components/guardrails/README.md | 37 +- .../guardrails/add_guardrail_form.tsx | 317 ++-- .../azure_text_moderation_configuration.tsx | 112 +- .../azure_text_moderation_example.tsx | 64 +- .../guardrails/azure_text_moderation_types.ts | 9 +- .../guardrails/edit_guardrail_form.tsx | 197 +-- .../components/guardrails/guardrail_info.tsx | 322 ++-- .../guardrails/guardrail_info_helpers.tsx | 158 +- .../guardrails/guardrail_optional_params.tsx | 167 +- .../guardrails/guardrail_provider_fields.tsx | 58 +- .../guardrail_provider_specific_fields.tsx | 97 +- .../components/guardrails/guardrail_table.tsx | 122 +- .../components/guardrails/pii_components.tsx | 99 +- .../guardrails/pii_configuration.tsx | 91 +- .../src/components/guardrails/types.ts | 6 +- .../src/components/key_info_utils.tsx | 15 +- .../fetch_available_models_team_key.tsx | 91 +- .../key_team_helpers/filter_helpers.ts | 25 +- .../key_team_helpers/filter_logic.tsx | 85 +- .../components/key_team_helpers/key_list.tsx | 358 +++-- .../organization_search_fn.tsx | 41 +- .../key_team_helpers/team_search_fn.tsx | 33 +- .../key_team_helpers/transform_key_info.tsx | 6 +- .../src/components/key_value_input.tsx | 23 +- .../src/components/leftnav.tsx | 272 ++-- .../src/components/logging_settings_view.tsx | 46 +- .../src/components/make_model_public_form.tsx | 73 +- .../src/components/mcp_connection_test.tsx | 239 +-- .../MCPServerSelector.tsx | 93 +- .../mcp_tools/MCPPermissionManagement.tsx | 49 +- .../mcp_tools/StdioConfiguration.tsx | 4 +- .../components/mcp_tools/ToolTestPanel.tsx | 332 ++-- .../src/components/mcp_tools/code-example.tsx | 12 +- .../mcp_tools/create_mcp_server.tsx | 190 +-- .../src/components/mcp_tools/index.tsx | 6 +- .../components/mcp_tools/mcp_auth_storage.ts | 31 +- .../src/components/mcp_tools/mcp_connect.tsx | 239 ++- .../mcp_tools/mcp_connection_status.tsx | 30 +- .../mcp_tools/mcp_server_columns.tsx | 55 +- .../mcp_tools/mcp_server_cost_config.tsx | 52 +- .../mcp_tools/mcp_server_cost_display.tsx | 53 +- .../components/mcp_tools/mcp_server_edit.tsx | 85 +- .../components/mcp_tools/mcp_server_view.tsx | 72 +- .../src/components/mcp_tools/mcp_servers.tsx | 198 +-- .../mcp_tools/mcp_tool_configuration.tsx | 88 +- .../src/components/mcp_tools/mcp_tools.tsx | 147 +- .../src/components/mcp_tools/types.tsx | 156 +- .../src/components/mcp_tools/utils.tsx | 20 +- .../model_add/CredentialDeleteModal.tsx | 19 +- .../model_add/add_credentials_tab.tsx | 54 +- .../src/components/model_add/credentials.tsx | 72 +- .../src/components/model_add/dynamic_form.tsx | 9 +- .../model_add/reuse_credentials.tsx | 54 +- .../model_dashboard/HealthCheckComponent.tsx | 369 ++--- .../model_dashboard/health_check_columns.tsx | 138 +- .../src/components/model_dashboard/table.tsx | 68 +- .../src/components/model_filters.tsx | 122 +- .../components/model_group_alias_settings.tsx | 94 +- .../src/components/model_hub_table.tsx | 159 +- .../components/model_hub_table_columns.tsx | 391 +++-- .../src/components/model_info_view.tsx | 401 ++--- .../src/components/molecules/filter.tsx | 57 +- .../components/molecules/models/columns.tsx | 94 +- .../molecules/notifications_manager.tsx | 161 +- .../src/components/navbar.tsx | 95 +- .../src/components/networking.test.ts | 16 +- .../src/components/networking.tsx | 1171 ++++---------- .../src/components/new_usage.tsx | 342 ++-- .../components/object_permissions_view.tsx | 21 +- .../src/components/onboarding_link.tsx | 17 +- .../organisms/create_key_button.tsx | 868 +++++------ .../organisms/regenerate_key_modal.tsx | 149 +- .../components/organization/add_org_admin.tsx | 240 ++- .../organization/organization_view.tsx | 196 +-- .../src/components/organization/types.tsx | 31 +- .../organization/view_members_of_org.tsx | 45 +- .../src/components/organizations.tsx | 160 +- .../src/components/pass_through_info.tsx | 103 +- .../src/components/pass_through_settings.tsx | 123 +- .../src/components/per_user_usage.tsx | 95 +- .../permissions/MCPServerPermissions.tsx | 39 +- .../permissions/VectorStorePermissions.tsx | 23 +- .../src/components/price_data_reload.tsx | 88 +- .../src/components/prompts.tsx | 109 +- .../src/components/prompts/README.md | 20 +- .../components/prompts/add_prompt_form.tsx | 188 +-- .../src/components/prompts/index.ts | 4 +- .../src/components/prompts/prompt_info.tsx | 142 +- .../src/components/prompts/prompt_table.tsx | 120 +- .../src/components/provider_info_helpers.tsx | 406 ++--- .../src/components/public_model_hub.tsx | 845 +++++----- .../components/public_model_hub_columns.tsx | 52 +- .../src/components/request_model_access.tsx | 24 +- .../components/response_time_indicator.tsx | 11 +- .../src/components/route_preview.tsx | 33 +- .../src/components/settings.tsx | 257 ++- .../shared/advanced_date_picker.tsx | 204 +-- .../src/components/shared/chart_loader.tsx | 2 +- .../src/components/shared/errorUtils.tsx | 24 +- .../src/components/shared/numerical_input.tsx | 4 +- .../components/shared/usage_date_picker.tsx | 80 +- .../components/tag_management/TagSelector.tsx | 14 +- .../components/tag_management/TagTable.tsx | 77 +- .../src/components/tag_management/index.tsx | 92 +- .../components/tag_management/tag_info.tsx | 82 +- .../src/components/tag_management/types.tsx | 6 +- .../components/team/EditLoggingSettings.tsx | 16 +- .../components/team/LoggingSettings.test.tsx | 144 +- .../src/components/team/LoggingSettings.tsx | 104 +- .../src/components/team/available_teams.tsx | 54 +- .../src/components/team/edit_membership.tsx | 118 +- .../components/team/member_permissions.tsx | 100 +- .../team/permission_definitions.tsx | 39 +- .../src/components/team/team_info.tsx | 237 +-- .../src/components/team/team_member_view.tsx | 198 +-- ui/litellm-dashboard/src/components/teams.tsx | 629 +++----- .../KeyInfoView.handleKeyUpdate.test.tsx | 273 ++-- .../components/templates/key_edit_view.tsx | 188 ++- .../components/templates/key_info_view.tsx | 351 +++-- .../components/templates/model_dashboard.tsx | 782 +++++----- .../components/templates/view_key_table.tsx | 481 +++--- .../src/components/top_key_view.tsx | 157 +- .../src/components/transform_request.tsx | 342 ++-- .../src/components/ui/ui-loading-spinner.tsx | 27 +- .../src/components/ui_theme_settings.tsx | 67 +- ui/litellm-dashboard/src/components/usage.tsx | 966 ++++++------ .../src/components/usage/types.ts | 122 +- .../src/components/usage_indicator.tsx | 326 ++-- .../components/useful_links_management.tsx | 105 +- .../src/components/user_agent_activity.tsx | 131 +- .../src/components/user_dashboard.tsx | 314 ++-- .../src/components/user_edit_view.tsx | 101 +- .../vector_store_management/DeleteModal.tsx | 25 +- .../VectorStoreForm.tsx | 159 +- .../VectorStoreSelector.tsx | 28 +- .../VectorStoreTable.tsx | 77 +- .../VectorStoreTester.tsx | 349 +++-- .../vector_store_management/index.tsx | 38 +- .../vector_store_management/types.tsx | 2 +- .../vector_store_info.tsx | 139 +- .../src/components/vector_store_providers.tsx | 174 +-- .../view_logs/ConfigInfoMessage.tsx | 23 +- .../src/components/view_logs/ErrorViewer.tsx | 90 +- .../BedrockGuardrailDetails.test.tsx | 84 +- .../BedrockGuardrailDetails.tsx | 141 +- .../GuardrailViewer/GuardrailViewer.test.tsx | 99 +- .../GuardrailViewer/GuardrailViewer.tsx | 26 +- .../PresidioDetectedEntities.test.tsx | 50 +- .../PresidioDetectedEntities.tsx | 19 +- .../GuardrailViewer/__tests__/fixtures.ts | 50 +- .../view_logs/RequestResponsePanel.tsx | 72 +- .../src/components/view_logs/SessionView.tsx | 88 +- .../view_logs/VectorStoreViewer.tsx | 42 +- .../src/components/view_logs/audit_logs.tsx | 378 ++--- .../src/components/view_logs/columns.tsx | 163 +- .../src/components/view_logs/country_cell.tsx | 10 +- .../src/components/view_logs/index.tsx | 388 ++--- .../src/components/view_logs/ip_lookup.tsx | 16 +- .../components/view_logs/log_filter_logic.tsx | 193 ++- .../src/components/view_logs/logs_utils.tsx | 28 +- .../src/components/view_logs/prefetch.ts | 14 +- .../src/components/view_logs/table.tsx | 57 +- .../src/components/view_logs/time_cell.tsx | 34 +- .../view_model/model_name_display.tsx | 22 +- .../src/components/view_user_spend.tsx | 198 +-- .../src/components/view_user_team.tsx | 105 +- .../src/components/view_users.tsx | 316 ++-- .../src/components/view_users/columns.tsx | 232 ++- .../src/components/view_users/table.tsx | 252 +-- .../src/components/view_users/types.ts | 2 +- .../components/view_users/user_info_view.tsx | 197 +-- .../src/contexts/ThemeContext.tsx | 26 +- .../src/hooks/use-safe-layout-effect.ts | 4 +- .../src/hooks/useTestMCPConnection.tsx | 102 +- ui/litellm-dashboard/src/lib/cva.config.ts | 4 +- ui/litellm-dashboard/src/types.ts | 4 +- .../src/utils/cookieUtils.test.ts | 72 +- ui/litellm-dashboard/src/utils/cookieUtils.ts | 28 +- ui/litellm-dashboard/src/utils/dataUtils.ts | 68 +- .../src/utils/errorPatterns.ts | 22 +- ui/litellm-dashboard/src/utils/proxyUtils.ts | 4 +- ui/litellm-dashboard/src/utils/roles.ts | 16 +- ui/litellm-dashboard/tailwind.config.js | 14 +- ui/litellm-dashboard/tailwind.config.ts | 13 +- ui/litellm-dashboard/tests/setupTests.ts | 10 +- ui/litellm-dashboard/tests/test-utils.tsx | 6 +- .../tests/top_key_view.test.tsx | 313 ++-- .../tests/utils/dataUtils.test.ts | 260 ++-- .../view_logs/useLogFilterLogic.min.test.tsx | 21 +- ui/litellm-dashboard/ui_colors.json | 15 +- ui/litellm-dashboard/vitest.config.ts | 20 +- 298 files changed, 17336 insertions(+), 20204 deletions(-) create mode 100644 ui/litellm-dashboard/.prettierignore create mode 100644 ui/litellm-dashboard/.prettierrc delete mode 100644 ui/litellm-dashboard/.prettierrc.json diff --git a/ui/litellm-dashboard/.prettierignore b/ui/litellm-dashboard/.prettierignore new file mode 100644 index 00000000000..ab37c884be1 --- /dev/null +++ b/ui/litellm-dashboard/.prettierignore @@ -0,0 +1,11 @@ +node_modules +.next +.out +dist +build +.coverage +.vercel +.turbo +.next-static +*.min.js +coverage/ \ No newline at end of file diff --git a/ui/litellm-dashboard/.prettierrc b/ui/litellm-dashboard/.prettierrc new file mode 100644 index 00000000000..6ce0705cb78 --- /dev/null +++ b/ui/litellm-dashboard/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/ui/litellm-dashboard/.prettierrc.json b/ui/litellm-dashboard/.prettierrc.json deleted file mode 100644 index 69cb9796325..00000000000 --- a/ui/litellm-dashboard/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": false, - "tabWidth": 2, - "printWidth": 120, - "trailingComma": "all", - "jsxBracketSameLine": false -} \ No newline at end of file diff --git a/ui/litellm-dashboard/next.config.mjs b/ui/litellm-dashboard/next.config.mjs index 540849269e5..f3083c5e802 100644 --- a/ui/litellm-dashboard/next.config.mjs +++ b/ui/litellm-dashboard/next.config.mjs @@ -1,12 +1,12 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'export', - basePath: '', - assetPrefix: '/litellm-asset-prefix', // If a server_root_path is set, this will be overridden by runtime injection + output: "export", + basePath: "", + assetPrefix: "/litellm-asset-prefix", // If a server_root_path is set, this will be overridden by runtime injection }; nextConfig.experimental = { - missingSuspenseWithCSRBailout: false -} + missingSuspenseWithCSRBailout: false, +}; export default nextConfig; diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 9ab39c57bfd..b4aa6fc6b76 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -17973,6 +17973,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 30c35591d6d..bddc866d744 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -8,7 +8,9 @@ "start": "next start", "lint": "next lint", "test": "vitest", - "test:watch": "vitest -w" + "test:watch": "vitest -w", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@anthropic-ai/sdk": "^0.54.0", diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css index 8b9aa9e6706..a702982678e 100644 --- a/ui/litellm-dashboard/src/app/globals.css +++ b/ui/litellm-dashboard/src/app/globals.css @@ -19,12 +19,7 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); } @layer utilities { diff --git a/ui/litellm-dashboard/src/app/model_hub/page.tsx b/ui/litellm-dashboard/src/app/model_hub/page.tsx index 76e9fc342b8..265fe485ec4 100644 --- a/ui/litellm-dashboard/src/app/model_hub/page.tsx +++ b/ui/litellm-dashboard/src/app/model_hub/page.tsx @@ -19,7 +19,5 @@ export default function PublicModelHub() { * populate navbar * */ - return ( - - ); + return ; } diff --git a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx index 196279a7852..d10f11d61bf 100644 --- a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx +++ b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx @@ -19,7 +19,5 @@ export default function PublicModelHubTable() { * populate navbar * */ - return ( - - ); -} \ No newline at end of file + return ; +} diff --git a/ui/litellm-dashboard/src/app/onboarding/page.tsx b/ui/litellm-dashboard/src/app/onboarding/page.tsx index 4e2f8903188..f37d69f7989 100644 --- a/ui/litellm-dashboard/src/app/onboarding/page.tsx +++ b/ui/litellm-dashboard/src/app/onboarding/page.tsx @@ -1,16 +1,7 @@ "use client"; import React, { Suspense, useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; -import { - Card, - Title, - Text, - TextInput, - Callout, - Button, - Grid, - Col, -} from "@tremor/react"; +import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react"; import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react"; import { invitationClaimCall, @@ -18,7 +9,7 @@ import { getOnboardingCredentials, claimOnboardingToken, getUiConfig, - getProxyBaseUrl + getProxyBaseUrl, } from "@/components/networking"; import { jwtDecode } from "jwt-decode"; import { Form, Button as Button2, message } from "antd"; @@ -27,7 +18,7 @@ import { getCookie } from "@/utils/cookieUtils"; export default function Onboarding() { const [form] = Form.useForm(); const searchParams = useSearchParams()!; - const token = getCookie('token'); + const token = getCookie("token"); const inviteID = searchParams.get("invitation_id"); const action = searchParams.get("action"); const [accessToken, setAccessToken] = useState(null); @@ -39,14 +30,16 @@ export default function Onboarding() { const [getUiConfigLoading, setGetUiConfigLoading] = useState(true); useEffect(() => { - getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading + getUiConfig().then((data) => { + // get the information for constructing the proxy base url, and then set the token and auth loading console.log("ui config in onboarding.tsx:", data); setGetUiConfigLoading(false); }); }, []); useEffect(() => { - if (!inviteID || getUiConfigLoading) { // wait for the ui config to be loaded + if (!inviteID || getUiConfigLoading) { + // wait for the ui config to be loaded return; } @@ -72,14 +65,7 @@ export default function Onboarding() { }, [inviteID, getUiConfigLoading]); const handleSubmit = (formValues: Record) => { - console.log( - "in handle submit. accessToken:", - accessToken, - "token:", - jwtToken, - "formValues:", - formValues - ); + console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues); if (!accessToken || !jwtToken) { return; } @@ -89,12 +75,7 @@ export default function Onboarding() { if (!userID || !inviteID) { return; } - claimOnboardingToken( - accessToken, - inviteID, - userID, - formValues.password - ).then((data) => { + claimOnboardingToken(accessToken, inviteID, userID, formValues.password).then((data) => { let litellm_dashboard_ui = "/ui/"; litellm_dashboard_ui += "?login=success"; @@ -119,15 +100,14 @@ export default function Onboarding() { 🚅 LiteLLM {action === "reset_password" ? "Reset Password" : "Sign up"} - {action === "reset_password" ? "Reset your password to access Admin UI." : "Claim your user account to login to Admin UI."} + + {action === "reset_password" + ? "Reset your password to access Admin UI." + : "Claim your user account to login to Admin UI."} + {action !== "reset_password" && ( - + SSO is under the Enterprise Tier. @@ -142,28 +122,16 @@ export default function Onboarding() { )} -
+ <> - + diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 0503e5c6086..579e90e530c 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,85 +1,85 @@ -"use client" +"use client"; -import React, { Suspense, useEffect, useState } from "react" -import { useSearchParams } from "next/navigation" -import { jwtDecode } from "jwt-decode" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { Team } from "@/components/key_team_helpers/key_list" -import Navbar from "@/components/navbar" -import { ThemeProvider } from "@/contexts/ThemeContext" -import UserDashboard from "@/components/user_dashboard" -import ModelDashboard from "@/components/templates/model_dashboard" -import ViewUserDashboard from "@/components/view_users" -import Teams from "@/components/teams" -import Organizations from "@/components/organizations" -import { fetchOrganizations } from "@/components/organizations" -import AdminPanel from "@/components/admins" -import Settings from "@/components/settings" -import GeneralSettings from "@/components/general_settings" -import PassThroughSettings from "@/components/pass_through_settings" -import BudgetPanel from "@/components/budgets/budget_panel" -import SpendLogsTable from "@/components/view_logs" -import ModelHubTable from "@/components/model_hub_table" -import NewUsagePage from "@/components/new_usage" -import APIRef from "@/components/api_ref" -import ChatUI from "@/components/chat_ui/ChatUI" -import Sidebar from "@/components/leftnav" -import Usage from "@/components/usage" -import CacheDashboard from "@/components/cache_dashboard" -import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } 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/organisms/create_key_button" -import { fetchTeams } from "@/components/common_components/fetch_teams" -import { MCPServers } from "@/components/mcp_tools" -import TagManagement from "@/components/tag_management" -import VectorStoreManagement from "@/components/vector_store_management" -import UIThemeSettings from "@/components/ui_theme_settings" -import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner" -import { cx } from "@/lib/cva.config" +import React, { Suspense, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { jwtDecode } from "jwt-decode"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Team } from "@/components/key_team_helpers/key_list"; +import Navbar from "@/components/navbar"; +import { ThemeProvider } from "@/contexts/ThemeContext"; +import UserDashboard from "@/components/user_dashboard"; +import ModelDashboard from "@/components/templates/model_dashboard"; +import ViewUserDashboard from "@/components/view_users"; +import Teams from "@/components/teams"; +import Organizations from "@/components/organizations"; +import { fetchOrganizations } from "@/components/organizations"; +import AdminPanel from "@/components/admins"; +import Settings from "@/components/settings"; +import GeneralSettings from "@/components/general_settings"; +import PassThroughSettings from "@/components/pass_through_settings"; +import BudgetPanel from "@/components/budgets/budget_panel"; +import SpendLogsTable from "@/components/view_logs"; +import ModelHubTable from "@/components/model_hub_table"; +import NewUsagePage from "@/components/new_usage"; +import APIRef from "@/components/api_ref"; +import ChatUI from "@/components/chat_ui/ChatUI"; +import Sidebar from "@/components/leftnav"; +import Usage from "@/components/usage"; +import CacheDashboard from "@/components/cache_dashboard"; +import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } 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/organisms/create_key_button"; +import { fetchTeams } from "@/components/common_components/fetch_teams"; +import { MCPServers } from "@/components/mcp_tools"; +import TagManagement from "@/components/tag_management"; +import VectorStoreManagement from "@/components/vector_store_management"; +import UIThemeSettings from "@/components/ui_theme_settings"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { cx } from "@/lib/cva.config"; function getCookie(name: string) { - const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")) - return cookieValue ? cookieValue.split("=")[1] : null + const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); + return cookieValue ? cookieValue.split("=")[1] : null; } function formatUserRole(userRole: string) { if (!userRole) { - return "Undefined Role" + return "Undefined Role"; } switch (userRole.toLowerCase()) { case "app_owner": - return "App Owner" + return "App Owner"; case "demo_app_owner": - return "App Owner" + return "App Owner"; case "app_admin": - return "Admin" + return "Admin"; case "proxy_admin": - return "Admin" + return "Admin"; case "proxy_admin_viewer": - return "Admin Viewer" + return "Admin Viewer"; case "org_admin": - return "Org Admin" + return "Org Admin"; case "internal_user": - return "Internal User" + return "Internal User"; case "internal_user_viewer": case "internal_viewer": // TODO:remove if deprecated - return "Internal Viewer" + return "Internal Viewer"; case "app_user": - return "App User" + return "App User"; default: - return "Unknown Role" + return "Unknown Role"; } } interface ProxySettings { - PROXY_BASE_URL: string - PROXY_LOGOUT_URL: string + PROXY_BASE_URL: string; + PROXY_LOGOUT_URL: string; } -const queryClient = new QueryClient() +const queryClient = new QueryClient(); function LoadingScreen() { return ( @@ -91,51 +91,51 @@ function LoadingScreen() { Loading... - ) + ); } export default function CreateKeyPage() { - const [userRole, setUserRole] = useState("") - const [premiumUser, setPremiumUser] = useState(false) - const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false) - const [userEmail, setUserEmail] = useState(null) - const [teams, setTeams] = useState(null) - const [keys, setKeys] = useState([]) - const [organizations, setOrganizations] = useState([]) - const [userModels, setUserModels] = useState([]) + const [userRole, setUserRole] = useState(""); + const [premiumUser, setPremiumUser] = useState(false); + const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false); + const [userEmail, setUserEmail] = useState(null); + const [teams, setTeams] = useState(null); + const [keys, setKeys] = useState([]); + const [organizations, setOrganizations] = useState([]); + const [userModels, setUserModels] = useState([]); const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "", - }) + }); - const [showSSOBanner, setShowSSOBanner] = useState(true) - const searchParams = useSearchParams()! - const [modelData, setModelData] = useState({ data: [] }) - const [token, setToken] = useState(null) - const [createClicked, setCreateClicked] = useState(false) - const [authLoading, setAuthLoading] = useState(true) - const [userID, setUserID] = useState(null) + const [showSSOBanner, setShowSSOBanner] = useState(true); + const searchParams = useSearchParams()!; + const [modelData, setModelData] = useState({ data: [] }); + const [token, setToken] = useState(null); + const [createClicked, setCreateClicked] = useState(false); + const [authLoading, setAuthLoading] = useState(true); + const [userID, setUserID] = useState(null); - const invitation_id = searchParams.get("invitation_id") + const invitation_id = searchParams.get("invitation_id"); // Get page from URL, default to 'api-keys' if not present const [page, setPage] = useState(() => { - return searchParams.get("page") || "api-keys" - }) + return searchParams.get("page") || "api-keys"; + }); // Custom setPage function that updates URL const updatePage = (newPage: string) => { // Update URL without full page reload - const newSearchParams = new URLSearchParams(searchParams) - newSearchParams.set("page", newPage) + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("page", newPage); // Use Next.js router to update URL - window.history.pushState(null, "", `?${newSearchParams.toString()}`) + window.history.pushState(null, "", `?${newSearchParams.toString()}`); - setPage(newPage) - } + setPage(newPage); + }; - const [accessToken, setAccessToken] = useState(null) + const [accessToken, setAccessToken] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const toggleSidebar = () => { @@ -143,83 +143,83 @@ export default function CreateKeyPage() { }; const addKey = (data: any) => { - setKeys((prevData) => (prevData ? [...prevData, data] : [data])) - setCreateClicked(() => !createClicked) - } - const redirectToLogin = authLoading === false && token === null && invitation_id === null + setKeys((prevData) => (prevData ? [...prevData, data] : [data])); + setCreateClicked(() => !createClicked); + }; + const redirectToLogin = authLoading === false && token === null && invitation_id === null; useEffect(() => { - const token = getCookie("token") + const token = getCookie("token"); getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading - setToken(token) - setAuthLoading(false) - }) - }, []) + setToken(token); + setAuthLoading(false); + }); + }, []); useEffect(() => { if (redirectToLogin) { - window.location.href = (proxyBaseUrl || "") + "/sso/key/generate" + window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; } - }, [redirectToLogin]) + }, [redirectToLogin]); useEffect(() => { if (!token) { - return + return; } - const decoded = jwtDecode(token) as { [key: string]: any } + const decoded = jwtDecode(token) as { [key: string]: any }; if (decoded) { // set accessToken - setAccessToken(decoded.key) + setAccessToken(decoded.key); - setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation) + setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); // check if userRole is defined if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role) - setUserRole(formattedUserRole) + const formattedUserRole = formatUserRole(decoded.user_role); + setUserRole(formattedUserRole); if (formattedUserRole == "Admin Viewer") { - setPage("usage") + setPage("usage"); } } if (decoded.user_email) { - setUserEmail(decoded.user_email) + setUserEmail(decoded.user_email); } if (decoded.login_method) { - setShowSSOBanner(decoded.login_method == "username_password" ? true : false) + setShowSSOBanner(decoded.login_method == "username_password" ? true : false); } if (decoded.premium_user) { - setPremiumUser(decoded.premium_user) + setPremiumUser(decoded.premium_user); } if (decoded.auth_header_name) { - setGlobalLitellmHeaderName(decoded.auth_header_name) + setGlobalLitellmHeaderName(decoded.auth_header_name); } if (decoded.user_id) { - setUserID(decoded.user_id) + setUserID(decoded.user_id); } } - }, [token]) + }, [token]); useEffect(() => { if (accessToken && userID && userRole) { - fetchUserModels(userID, userRole, accessToken, setUserModels) + fetchUserModels(userID, userRole, accessToken, setUserModels); } if (accessToken && userID && userRole) { - fetchTeams(accessToken, userID, userRole, null, setTeams) + fetchTeams(accessToken, userID, userRole, null, setTeams); } if (accessToken) { - fetchOrganizations(accessToken, setOrganizations) + fetchOrganizations(accessToken, setOrganizations); } - }, [accessToken, userID, userRole]) + }, [accessToken, userID, userRole]); if (authLoading || redirectToLogin) { - return + return ; } return ( @@ -425,5 +425,5 @@ export default function CreateKeyPage() { - ) + ); } diff --git a/ui/litellm-dashboard/src/components/SCIM.tsx b/ui/litellm-dashboard/src/components/SCIM.tsx index fa083c43818..5ccfecaf197 100644 --- a/ui/litellm-dashboard/src/components/SCIM.tsx +++ b/ui/litellm-dashboard/src/components/SCIM.tsx @@ -1,24 +1,14 @@ import React, { useState, useEffect } from "react"; -import { - Card, - Title, - Text, - Grid, - Col, - Button as TremorButton, - Callout, - TextInput, - Divider, -} from "@tremor/react"; +import { Card, Title, Text, Grid, Col, Button as TremorButton, Callout, TextInput, Divider } from "@tremor/react"; import { message, Form } from "antd"; import { keyCreateCall } from "./networking"; import { CopyToClipboard } from "react-copy-to-clipboard"; -import { - LinkOutlined, - KeyOutlined, +import { + LinkOutlined, + KeyOutlined, CopyOutlined, ExclamationCircleOutlined, - PlusCircleOutlined + PlusCircleOutlined, } from "@ant-design/icons"; import { parseErrorMessage } from "./shared/errorUtils"; import NotificationsManager from "./molecules/notifications_manager"; @@ -34,38 +24,38 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti const [isCreatingToken, setIsCreatingToken] = useState(false); const [tokenData, setTokenData] = useState(null); const [baseUrl, setBaseUrl] = useState(""); - + useEffect(() => { let url = ""; - + if (proxySettings && proxySettings.PROXY_BASE_URL && proxySettings.PROXY_BASE_URL !== undefined) { url = proxySettings.PROXY_BASE_URL; - } else if (typeof window !== 'undefined') { + } else if (typeof window !== "undefined") { // Use the current origin as the base URL if no proxy URL is set url = window.location.origin; } - + setBaseUrl(url); }, [proxySettings]); - + const scimBaseUrl = `${baseUrl}/scim/v2`; - + const handleCreateSCIMToken = async (values: any) => { if (!accessToken || !userID) { NotificationsManager.fromBackend("You need to be logged in to create a SCIM token"); return; } - + try { setIsCreatingToken(true); - + const formData = { key_alias: values.key_alias || "SCIM Access Token", team_id: null, models: [], allowed_routes: ["/scim/*"], }; - + const response = await keyCreateCall(accessToken, userID, formData); setTokenData(response); NotificationsManager.success("SCIM token created successfully"); @@ -84,11 +74,12 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti SCIM Configuration - System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and groups in LiteLLM. + System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and + groups in LiteLLM. - + - +
{/* Step 1: SCIM URL */}
@@ -105,11 +96,7 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti Use this URL in your identity provider SCIM integration settings.
- + NotificationsManager.success("URL copied to clipboard")} @@ -133,18 +120,15 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti Authentication Token
- + - You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider configuration. + You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider + configuration. {!tokenData ? (
- + = ({ accessToken, userID, proxySetti
- setTokenData(null)} - > + setTokenData(null)}> Create Another Token @@ -208,4 +188,4 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti ); }; -export default SCIMConfig; \ No newline at end of file +export default SCIMConfig; diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 56ec9393fa4..44b09dc4db0 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -38,56 +38,64 @@ interface SSOProviderConfig { const ssoProviderConfigs: Record = { google: { envVarMap: { - google_client_id: 'GOOGLE_CLIENT_ID', - google_client_secret: 'GOOGLE_CLIENT_SECRET', + google_client_id: "GOOGLE_CLIENT_ID", + google_client_secret: "GOOGLE_CLIENT_SECRET", }, fields: [ - { label: 'GOOGLE CLIENT ID', name: 'google_client_id' }, - { label: 'GOOGLE CLIENT SECRET', name: 'google_client_secret' }, + { label: "GOOGLE CLIENT ID", name: "google_client_id" }, + { label: "GOOGLE CLIENT SECRET", name: "google_client_secret" }, ], }, microsoft: { envVarMap: { - microsoft_client_id: 'MICROSOFT_CLIENT_ID', - microsoft_client_secret: 'MICROSOFT_CLIENT_SECRET', - microsoft_tenant: 'MICROSOFT_TENANT', + microsoft_client_id: "MICROSOFT_CLIENT_ID", + microsoft_client_secret: "MICROSOFT_CLIENT_SECRET", + microsoft_tenant: "MICROSOFT_TENANT", }, fields: [ - { label: 'MICROSOFT CLIENT ID', name: 'microsoft_client_id' }, - { label: 'MICROSOFT CLIENT SECRET', name: 'microsoft_client_secret' }, - { label: 'MICROSOFT TENANT', name: 'microsoft_tenant' }, + { label: "MICROSOFT CLIENT ID", name: "microsoft_client_id" }, + { label: "MICROSOFT CLIENT SECRET", name: "microsoft_client_secret" }, + { label: "MICROSOFT TENANT", name: "microsoft_tenant" }, ], }, okta: { envVarMap: { - generic_client_id: 'GENERIC_CLIENT_ID', - generic_client_secret: 'GENERIC_CLIENT_SECRET', - generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT', - generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT', - generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT', + generic_client_id: "GENERIC_CLIENT_ID", + generic_client_secret: "GENERIC_CLIENT_SECRET", + generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT", + generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT", + generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT", }, fields: [ - { label: 'GENERIC CLIENT ID', name: 'generic_client_id' }, - { label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' }, - { label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint', placeholder: 'https://your-okta-domain/authorize' }, - { label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint', placeholder: 'https://your-okta-domain/token' }, - { label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint', placeholder: 'https://your-okta-domain/userinfo' }, + { label: "GENERIC CLIENT ID", name: "generic_client_id" }, + { label: "GENERIC CLIENT SECRET", name: "generic_client_secret" }, + { + label: "AUTHORIZATION ENDPOINT", + name: "generic_authorization_endpoint", + placeholder: "https://your-okta-domain/authorize", + }, + { label: "TOKEN ENDPOINT", name: "generic_token_endpoint", placeholder: "https://your-okta-domain/token" }, + { + label: "USERINFO ENDPOINT", + name: "generic_userinfo_endpoint", + placeholder: "https://your-okta-domain/userinfo", + }, ], }, generic: { envVarMap: { - generic_client_id: 'GENERIC_CLIENT_ID', - generic_client_secret: 'GENERIC_CLIENT_SECRET', - generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT', - generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT', - generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT', + generic_client_id: "GENERIC_CLIENT_ID", + generic_client_secret: "GENERIC_CLIENT_SECRET", + generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT", + generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT", + generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT", }, fields: [ - { label: 'GENERIC CLIENT ID', name: 'generic_client_id' }, - { label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' }, - { label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint' }, - { label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint' }, - { label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint' }, + { label: "GENERIC CLIENT ID", name: "generic_client_id" }, + { label: "GENERIC CLIENT SECRET", name: "generic_client_secret" }, + { label: "AUTHORIZATION ENDPOINT", name: "generic_authorization_endpoint" }, + { label: "TOKEN ENDPOINT", name: "generic_token_endpoint" }, + { label: "USERINFO ENDPOINT", name: "generic_userinfo_endpoint" }, ], }, }; @@ -116,20 +124,22 @@ const SSOModals: React.FC = ({ if (ssoData && ssoData.values) { console.log("SSO values:", ssoData.values); // Debug log console.log("user_email from API:", ssoData.values.user_email); // Debug log - + // Determine which SSO provider is configured let selectedProvider = null; if (ssoData.values.google_client_id) { - selectedProvider = 'google'; + selectedProvider = "google"; } else if (ssoData.values.microsoft_client_id) { - selectedProvider = 'microsoft'; + selectedProvider = "microsoft"; } else if (ssoData.values.generic_client_id) { // Check if it looks like Okta based on endpoints - if (ssoData.values.generic_authorization_endpoint?.includes('okta') || - ssoData.values.generic_authorization_endpoint?.includes('auth0')) { - selectedProvider = 'okta'; + if ( + ssoData.values.generic_authorization_endpoint?.includes("okta") || + ssoData.values.generic_authorization_endpoint?.includes("auth0") + ) { + selectedProvider = "okta"; } else { - selectedProvider = 'generic'; + selectedProvider = "generic"; } } @@ -142,7 +152,7 @@ const SSOModals: React.FC = ({ }; console.log("Setting form values:", formValues); // Debug log - + // Clear form first, then set values with a small delay to ensure proper initialization form.resetFields(); setTimeout(() => { @@ -169,7 +179,7 @@ const SSOModals: React.FC = ({ try { // Save SSO settings using the new API await updateSSOSettings(accessToken, formValues); - + // Continue with the original flow (show instructions) handleShowInstructions(formValues); } catch (error) { @@ -204,16 +214,16 @@ const SSOModals: React.FC = ({ }; await updateSSOSettings(accessToken, clearSettings); - + // Clear the form form.resetFields(); - + // Close the confirmation modal setIsClearConfirmModalVisible(false); - + // Close the main SSO modal and trigger refresh handleAddSSOOk(); - + NotificationsManager.success("SSO settings cleared successfully"); } catch (error) { console.error("Failed to clear SSO settings:", error); @@ -233,11 +243,7 @@ const SSOModals: React.FC = ({ name={field.name} rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]} > - {field.name.includes('client') ? ( - - ) : ( - - )} + {field.name.includes("client") ? : } )); }; @@ -268,8 +274,14 @@ const SSOModals: React.FC = ({ updateTeam(index, "user_role", value)} > @@ -199,13 +202,8 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
))} - - @@ -214,17 +212,13 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, const renderEditableField = (key: string, property: any, value: any) => { const type = property.type; - + if (key === "teams") { - return ( -
- {renderTeamsEditor(editedValues[key] || [])} -
- ); + return
{renderTeamsEditor(editedValues[key] || [])}
; } else if (key === "user_role" && possibleUIRoles) { return ( handleTextInputChange(key, value)} className="mt-2" > {property.items.enum.map((option: string) => ( - + ))} ); @@ -276,7 +269,7 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, return ( handleTextInputChange(key, value)} className="mt-2" > {property.enum.map((option: string) => ( - + ))} ); } else { return ( - handleTextInputChange(key, e.target.value)} placeholder={property.description || ""} className="mt-2" @@ -316,12 +311,12 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, const renderValue = (key: string, value: any): JSX.Element => { if (value === null || value === undefined) return Not set; - + if (key === "teams" && Array.isArray(value)) { if (value.length === 0) return No teams assigned; - + const normalizedTeams = normalizeTeams(value); - + return (
{normalizedTeams.map((team, index) => ( @@ -334,8 +329,8 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
Max Budget:

- {team.max_budget_in_team !== undefined - ? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}` + {team.max_budget_in_team !== undefined + ? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}` : "No limit"}

@@ -349,7 +344,7 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - + if (key === "user_role" && possibleUIRoles && possibleUIRoles[value]) { const { ui_label, description } = possibleUIRoles[value]; return ( @@ -359,18 +354,18 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, ); } - + if (key === "budget_duration") { return {getBudgetDurationLabel(value)}; } - + if (typeof value === "boolean") { return {value ? "Enabled" : "Disabled"}; } - + if (key === "models" && Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((model, index) => ( @@ -381,11 +376,11 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - + if (typeof value === "object") { if (Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((item, index) => ( @@ -396,14 +391,10 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - - return ( -
-          {JSON.stringify(value, null, 2)}
-        
- ); + + return
{JSON.stringify(value, null, 2)}
; } - + return {String(value)}; }; @@ -426,30 +417,26 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, // Dynamically render settings based on the schema const renderSettings = () => { const { values, field_schema } = settings; - + if (!field_schema || !field_schema.properties) { return No schema information available; } return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => { const value = values[key]; - const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - + const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + return (
{displayName} {property.description || "No description available"} - + {isEditing ? ( -
- {renderEditableField(key, property, value)} -
+
{renderEditableField(key, property, value)}
) : ( -
- {renderValue(key, value)} -
+
{renderValue(key, value)}
)}
); @@ -460,10 +447,11 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
Default User Settings - {!loading && settings && ( - isEditing ? ( + {!loading && + settings && + (isEditing ? (
- -
) : ( - - ) - )} + + ))}
- + {settings?.field_schema?.description && ( {settings.field_schema.description} )} - -
- {renderSettings()} -
+ +
{renderSettings()}
); }; -export default SSOSettings; \ No newline at end of file +export default SSOSettings; diff --git a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx index 79f02d93f1a..4d20630e9f6 100644 --- a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx +++ b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx @@ -33,7 +33,7 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const data = await getDefaultTeamSettings(accessToken); setSettings(data); setEditedValues(data.values || {}); - + // Fetch available models if (accessToken) { try { @@ -59,11 +59,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const handleSaveSettings = async () => { if (!accessToken) return; - + setSaving(true); try { const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues); - setSettings({...settings, values: updatedSettings.settings}); + setSettings({ ...settings, values: updatedSettings.settings }); setIsEditing(false); NotificationsManager.success("Default team settings updated successfully"); } catch (error) { @@ -77,13 +77,13 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const handleTextInputChange = (key: string, value: any) => { setEditedValues((prev: Record) => ({ ...prev, - [key]: value + [key]: value, })); }; const renderEditableField = (key: string, property: any, value: any) => { const type = property.type; - + if (key === "budget_duration") { return ( = ({ accessToken, userID, } else if (type === "boolean") { return (
- handleTextInputChange(key, checked)} - /> + handleTextInputChange(key, checked)} />
); } else if (type === "array" && property.items?.enum) { return ( ); @@ -119,7 +118,7 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, return ( handleTextInputChange(key, value)} className="mt-2" > {property.enum.map((option: string) => ( - + ))} ); } else { return ( - handleTextInputChange(key, e.target.value)} placeholder={property.description || ""} className="mt-2" @@ -158,18 +159,18 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const renderValue = (key: string, value: any): JSX.Element => { if (value === null || value === undefined) return Not set; - + if (key === "budget_duration") { return {getBudgetDurationLabel(value)}; } - + if (typeof value === "boolean") { return {value ? "Enabled" : "Disabled"}; } - + if (key === "models" && Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((model, index) => ( @@ -180,11 +181,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
); } - + if (typeof value === "object") { if (Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((item, index) => ( @@ -195,14 +196,10 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
); } - - return ( -
-          {JSON.stringify(value, null, 2)}
-        
- ); + + return
{JSON.stringify(value, null, 2)}
; } - + return {String(value)}; }; @@ -225,30 +222,26 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, // Dynamically render settings based on the schema const renderSettings = () => { const { values, field_schema } = settings; - + if (!field_schema || !field_schema.properties) { return No schema information available; } return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => { const value = values[key]; - const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - + const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + return (
{displayName} {property.description || "No description available"} - + {isEditing ? ( -
- {renderEditableField(key, property, value)} -
+
{renderEditableField(key, property, value)}
) : ( -
- {renderValue(key, value)} -
+
{renderValue(key, value)}
)}
); @@ -259,10 +252,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
Default Team Settings - {!loading && settings && ( - isEditing ? ( + {!loading && + settings && + (isEditing ? (
- -
) : ( - - ) - )} + + ))}
- - - These settings will be applied by default when creating new teams. - - + + These settings will be applied by default when creating new teams. + {settings?.field_schema?.description && ( {settings.field_schema.description} )} - -
- {renderSettings()} -
+ +
{renderSettings()}
); }; -export default TeamSSOSettings; \ No newline at end of file +export default TeamSSOSettings; diff --git a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx index 5ef75d3835c..dacae9e9fcf 100644 --- a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx +++ b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx @@ -24,14 +24,14 @@ const UIAccessControlForm: React.FC = ({ accessToken, // Handle nested ui_access_mode structure const uiAccessMode = ssoData.values.ui_access_mode; let formValues = {}; - - if (uiAccessMode && typeof uiAccessMode === 'object') { + + if (uiAccessMode && typeof uiAccessMode === "object") { formValues = { ui_access_mode_type: uiAccessMode.type, restricted_sso_group: uiAccessMode.restricted_sso_group, sso_group_jwt_field: uiAccessMode.sso_group_jwt_field, }; - } else if (typeof uiAccessMode === 'string') { + } else if (typeof uiAccessMode === "string") { // Handle legacy flat structure formValues = { ui_access_mode_type: uiAccessMode, @@ -39,7 +39,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, sso_group_jwt_field: ssoData.values.team_ids_jwt_field || ssoData.values.sso_group_jwt_field, }; } - + form.setFieldsValue(formValues); } } catch (error) { @@ -61,11 +61,11 @@ const UIAccessControlForm: React.FC = ({ accessToken, try { // Transform form data to match API expected structure let apiPayload; - - if (formValues.ui_access_mode_type === 'all_authenticated_users') { + + if (formValues.ui_access_mode_type === "all_authenticated_users") { // Set ui_access_mode to none when all_authenticated_users is selected apiPayload = { - ui_access_mode: "none" + ui_access_mode: "none", }; } else { apiPayload = { @@ -73,7 +73,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, type: formValues.ui_access_mode_type, restricted_sso_group: formValues.restricted_sso_group, sso_group_jwt_field: formValues.sso_group_jwt_field, - } + }, }; } @@ -88,23 +88,15 @@ const UIAccessControlForm: React.FC = ({ accessToken, }; return ( -
-
- +
+
+ Configure who can access the UI interface and how group information is extracted from JWT tokens.
- - - + + + ({ value: name, label: name }))} + options={guardrailsList.map((name) => ({ value: name, label: name }))} /> - {customPricing && (
- +