mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
build(ui/): UI - Public Model Hub v2 (#12532)
Closes https://github.com/BerriAI/litellm/pull/12532
This commit is contained in:
parent
75d68a47d0
commit
8b4673fe84
15 changed files with 1522 additions and 60 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
from litellm_enterprise.types.proxy.proxy_server import CustomAuthSettings
|
||||
|
|
@ -20,3 +21,14 @@ class EnterpriseProxyConfig:
|
|||
global custom_auth_settings
|
||||
custom_auth_settings = await self.load_custom_auth_settings(general_settings)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_custom_docs_description() -> Optional[str]:
|
||||
from litellm.proxy.proxy_server import premium_user
|
||||
|
||||
docs_description: Optional[str] = None
|
||||
if premium_user:
|
||||
# check if premium_user has custom_docs_description
|
||||
docs_description = os.getenv("DOCS_DESCRIPTION")
|
||||
|
||||
return docs_description
|
||||
|
|
|
|||
|
|
@ -113,16 +113,24 @@ AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY = float(
|
|||
os.getenv("AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY", 0.1) # $0.1 USD per 1 GB/Day
|
||||
)
|
||||
AZURE_CODE_INTERPRETER_COST_PER_SESSION = float(
|
||||
os.getenv("AZURE_CODE_INTERPRETER_COST_PER_SESSION", 0.03) # $0.03 USD per 1 Session
|
||||
os.getenv(
|
||||
"AZURE_CODE_INTERPRETER_COST_PER_SESSION", 0.03
|
||||
) # $0.03 USD per 1 Session
|
||||
)
|
||||
AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS = float(
|
||||
os.getenv("AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS", 3.0) # $0.003 USD per 1K Tokens
|
||||
os.getenv(
|
||||
"AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS", 3.0
|
||||
) # $0.003 USD per 1K Tokens
|
||||
)
|
||||
AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS = float(
|
||||
os.getenv("AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS", 12.0) # $0.012 USD per 1K Tokens
|
||||
os.getenv(
|
||||
"AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS", 12.0
|
||||
) # $0.012 USD per 1K Tokens
|
||||
)
|
||||
AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY = float(
|
||||
os.getenv("AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY", 0.1) # $0.1 USD per 1 GB/Day (same as file search)
|
||||
os.getenv(
|
||||
"AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY", 0.1
|
||||
) # $0.1 USD per 1 GB/Day (same as file search)
|
||||
)
|
||||
MIN_NON_ZERO_TEMPERATURE = float(os.getenv("MIN_NON_ZERO_TEMPERATURE", 0.0001))
|
||||
#### RELIABILITY ####
|
||||
|
|
@ -394,7 +402,7 @@ openai_compatible_endpoints: List = [
|
|||
"api.featherless.ai/v1",
|
||||
"inference.api.nscale.com/v1",
|
||||
"api.studio.nebius.ai/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -430,7 +438,7 @@ openai_compatible_providers: List = [
|
|||
"featherless_ai",
|
||||
"nscale",
|
||||
"nebius",
|
||||
"dashscope"
|
||||
"dashscope",
|
||||
]
|
||||
openai_text_completion_compatible_providers: List = (
|
||||
[ # providers that support `/v1/completions`
|
||||
|
|
@ -441,7 +449,7 @@ openai_text_completion_compatible_providers: List = (
|
|||
"llamafile",
|
||||
"featherless_ai",
|
||||
"nebius",
|
||||
"dashscope"
|
||||
"dashscope",
|
||||
]
|
||||
)
|
||||
_openai_like_providers: List = [
|
||||
|
|
@ -625,7 +633,7 @@ dashscope_models: List = [
|
|||
"qwq-32b",
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-32b",
|
||||
"qwen3-30b-a3b"
|
||||
"qwen3-30b-a3b",
|
||||
]
|
||||
|
||||
nebius_embedding_models: List = [
|
||||
|
|
@ -816,7 +824,10 @@ LENGTH_OF_LITELLM_GENERATED_KEY = int(os.getenv("LENGTH_OF_LITELLM_GENERATED_KEY
|
|||
SECRET_MANAGER_REFRESH_INTERVAL = int(
|
||||
os.getenv("SECRET_MANAGER_REFRESH_INTERVAL", 86400)
|
||||
)
|
||||
LITELLM_SETTINGS_SAFE_DB_OVERRIDES = ["default_internal_user_params"]
|
||||
LITELLM_SETTINGS_SAFE_DB_OVERRIDES = [
|
||||
"default_internal_user_params",
|
||||
"public_model_groups",
|
||||
]
|
||||
SPECIAL_LITELLM_AUTH_TOKEN = ["ui-token"]
|
||||
DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL = int(
|
||||
os.getenv("DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL", 60)
|
||||
|
|
@ -825,25 +836,62 @@ DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL = int(
|
|||
# Sentry Scrubbing Configuration
|
||||
SENTRY_DENYLIST = [
|
||||
# API Keys and Tokens
|
||||
"api_key", "token", "key", "secret", "password", "auth", "credential",
|
||||
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AZURE_API_KEY", "COHERE_API_KEY",
|
||||
"REPLICATE_API_KEY", "HUGGINGFACE_API_KEY", "TOGETHERAI_API_KEY",
|
||||
"CLOUDFLARE_API_KEY", "BASETEN_KEY", "OPENROUTER_KEY", "DATAROBOT_API_TOKEN",
|
||||
"FIREWORKS_API_KEY", "FIREWORKS_AI_API_KEY", "FIREWORKSAI_API_KEY",
|
||||
"api_key",
|
||||
"token",
|
||||
"key",
|
||||
"secret",
|
||||
"password",
|
||||
"auth",
|
||||
"credential",
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"AZURE_API_KEY",
|
||||
"COHERE_API_KEY",
|
||||
"REPLICATE_API_KEY",
|
||||
"HUGGINGFACE_API_KEY",
|
||||
"TOGETHERAI_API_KEY",
|
||||
"CLOUDFLARE_API_KEY",
|
||||
"BASETEN_KEY",
|
||||
"OPENROUTER_KEY",
|
||||
"DATAROBOT_API_TOKEN",
|
||||
"FIREWORKS_API_KEY",
|
||||
"FIREWORKS_AI_API_KEY",
|
||||
"FIREWORKSAI_API_KEY",
|
||||
# Database and Connection Strings
|
||||
"database_url", "redis_url", "connection_string",
|
||||
"database_url",
|
||||
"redis_url",
|
||||
"connection_string",
|
||||
# Authentication and Security
|
||||
"master_key", "LITELLM_MASTER_KEY", "auth_token", "jwt_token", "private_key",
|
||||
"SLACK_WEBHOOK_URL", "webhook_url", "LANGFUSE_SECRET_KEY",
|
||||
"master_key",
|
||||
"LITELLM_MASTER_KEY",
|
||||
"auth_token",
|
||||
"jwt_token",
|
||||
"private_key",
|
||||
"SLACK_WEBHOOK_URL",
|
||||
"webhook_url",
|
||||
"LANGFUSE_SECRET_KEY",
|
||||
# Email Configuration
|
||||
"SMTP_PASSWORD", "SMTP_USERNAME", "email_password",
|
||||
"SMTP_PASSWORD",
|
||||
"SMTP_USERNAME",
|
||||
"email_password",
|
||||
# Cloud Provider Credentials
|
||||
"aws_access_key", "aws_secret_key", "gcp_credentials",
|
||||
"azure_credentials", "HCP_VAULT_TOKEN", "CIRCLE_OIDC_TOKEN",
|
||||
"aws_access_key",
|
||||
"aws_secret_key",
|
||||
"gcp_credentials",
|
||||
"azure_credentials",
|
||||
"HCP_VAULT_TOKEN",
|
||||
"CIRCLE_OIDC_TOKEN",
|
||||
# Proxy and Environment Settings
|
||||
"proxy_url", "proxy_key", "environment_variables"
|
||||
"proxy_url",
|
||||
"proxy_key",
|
||||
"environment_variables",
|
||||
]
|
||||
SENTRY_PII_DENYLIST = [
|
||||
"user_id", "email", "phone", "address", "ip_address",
|
||||
"SMTP_SENDER_EMAIL", "TEST_EMAIL_ADDRESS"
|
||||
]
|
||||
"user_id",
|
||||
"email",
|
||||
"phone",
|
||||
"address",
|
||||
"ip_address",
|
||||
"SMTP_SENDER_EMAIL",
|
||||
"TEST_EMAIL_ADDRESS",
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -13,8 +13,10 @@ router = APIRouter()
|
|||
"/litellm/.well-known/litellm-ui-config", response_model=UiDiscoveryEndpoints
|
||||
) # if mounted at root path
|
||||
async def get_ui_config():
|
||||
from litellm.proxy.proxy_server import _title, version
|
||||
from litellm.proxy.utils import get_proxy_base_url, get_server_root_path
|
||||
|
||||
return UiDiscoveryEndpoints(
|
||||
server_root_path=get_server_root_path(), proxy_base_url=get_proxy_base_url()
|
||||
server_root_path=get_server_root_path(),
|
||||
proxy_base_url=get_proxy_base_url(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import uuid
|
|||
from typing import Dict, List, Literal, Optional, Tuple, Union, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import LITELLM_PROXY_ADMIN_NAME
|
||||
|
|
@ -48,6 +48,7 @@ from litellm.types.router import (
|
|||
Deployment,
|
||||
DeploymentTypedDict,
|
||||
LiteLLMParamsTypedDict,
|
||||
ModelGroupInfo,
|
||||
updateDeployment,
|
||||
)
|
||||
from litellm.utils import get_utc_datetime
|
||||
|
|
@ -55,6 +56,16 @@ from litellm.utils import get_utc_datetime
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
class UpdatePublicModelGroupsRequest(BaseModel):
|
||||
"""Request model for updating public model groups"""
|
||||
|
||||
model_groups: List[str] = Field(
|
||||
description="List of model group names to make public"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
async def get_db_model(
|
||||
model_id: str, prisma_client: PrismaClient
|
||||
) -> Optional[Deployment]:
|
||||
|
|
@ -945,6 +956,86 @@ async def update_model(
|
|||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model_group/make_public",
|
||||
description="Update which model groups are public",
|
||||
tags=["model management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def update_public_model_groups(
|
||||
request: UpdatePublicModelGroupsRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update which model groups are public.
|
||||
|
||||
This endpoint allows admins to specify which model groups should be publicly accessible.
|
||||
Public model groups are visible via the /public/model_hub endpoint.
|
||||
|
||||
Args:
|
||||
request: Request containing list of model group names to make public
|
||||
user_api_key_dict: User authentication information
|
||||
|
||||
Returns:
|
||||
Success message with updated public model groups
|
||||
|
||||
Raises:
|
||||
ProxyException: For various error conditions including authentication errors
|
||||
"""
|
||||
try:
|
||||
# Update the public model groups
|
||||
import litellm
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Check if user has admin permissions
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins can update public model groups. Your role={}".format(
|
||||
user_api_key_dict.user_role
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
litellm.public_model_groups = request.model_groups
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
# Update config with new settings
|
||||
if "litellm_settings" not in config:
|
||||
config["litellm_settings"] = {}
|
||||
|
||||
config["litellm_settings"]["public_model_groups"] = request.model_groups
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Updated public model groups to: {request.model_groups} by user: {user_api_key_dict.user_id}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Successfully updated public model groups",
|
||||
"public_model_groups": request.model_groups,
|
||||
"updated_by": user_api_key_dict.user_id,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating public model groups: {str(e)}")
|
||||
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
|
||||
raise ProxyException(
|
||||
message=f"Error updating public model groups: {str(e)}",
|
||||
type=ProxyErrorTypes.internal_server_error,
|
||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
param=None,
|
||||
)
|
||||
|
||||
|
||||
def _deduplicate_litellm_router_models(models: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Deduplicate models based on their model_info.id field.
|
||||
|
|
|
|||
|
|
@ -355,6 +355,9 @@ from litellm.types.llms.anthropic import (
|
|||
AnthropicResponseUsageBlock,
|
||||
)
|
||||
from litellm.types.llms.openai import HttpxBinaryResponseContent
|
||||
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
|
||||
ModelGroupInfoProxy,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import (
|
||||
DefaultTeamSSOParams,
|
||||
LiteLLM_UpperboundKeyGenerateParams,
|
||||
|
|
@ -1640,7 +1643,7 @@ class ProxyConfig:
|
|||
if credential_list_dict:
|
||||
credential_list = [CredentialItem(**cred) for cred in credential_list_dict]
|
||||
return credential_list
|
||||
|
||||
|
||||
def _load_environment_variables(self, config: dict):
|
||||
## ENVIRONMENT VARIABLES
|
||||
global premium_user
|
||||
|
|
@ -1655,7 +1658,9 @@ class ProxyConfig:
|
|||
# ```
|
||||
#########################################################
|
||||
if isinstance(value, str) and value.startswith("os.environ/"):
|
||||
resolved_secret_string: Optional[str] = get_secret_str(secret_name=value)
|
||||
resolved_secret_string: Optional[str] = get_secret_str(
|
||||
secret_name=value
|
||||
)
|
||||
if resolved_secret_string is not None:
|
||||
os.environ[key] = resolved_secret_string
|
||||
else:
|
||||
|
|
@ -1974,10 +1979,13 @@ class ProxyConfig:
|
|||
value=custom_sso, config_file_path=config_file_path
|
||||
)
|
||||
|
||||
custom_ui_sso_sign_in_handler = general_settings.get("custom_ui_sso_sign_in_handler", None)
|
||||
custom_ui_sso_sign_in_handler = general_settings.get(
|
||||
"custom_ui_sso_sign_in_handler", None
|
||||
)
|
||||
if custom_ui_sso_sign_in_handler is not None:
|
||||
user_custom_ui_sso_sign_in_handler = get_instance_fn(
|
||||
value=custom_ui_sso_sign_in_handler, config_file_path=config_file_path
|
||||
value=custom_ui_sso_sign_in_handler,
|
||||
config_file_path=config_file_path,
|
||||
)
|
||||
|
||||
if enterprise_proxy_config is not None:
|
||||
|
|
@ -6530,8 +6538,8 @@ async def model_info_v1( # noqa: PLR0915
|
|||
|
||||
def _get_model_group_info(
|
||||
llm_router: Router, all_models_str: List[str], model_group: Optional[str]
|
||||
) -> List[ModelGroupInfo]:
|
||||
model_groups: List[ModelGroupInfo] = []
|
||||
) -> List[ModelGroupInfoProxy]:
|
||||
model_groups: List[ModelGroupInfoProxy] = []
|
||||
# ensure all_models_str is a set
|
||||
all_models_str_set = set(all_models_str)
|
||||
|
||||
|
|
@ -6542,15 +6550,23 @@ def _get_model_group_info(
|
|||
_model_group_info = llm_router.get_model_group_info(model_group=model)
|
||||
|
||||
if _model_group_info is not None:
|
||||
model_groups.append(_model_group_info)
|
||||
model_groups.append(ModelGroupInfoProxy(**_model_group_info.model_dump()))
|
||||
else:
|
||||
model_group_info = ModelGroupInfo(
|
||||
model_group_info = ModelGroupInfoProxy(
|
||||
model_group=model,
|
||||
providers=[],
|
||||
)
|
||||
model_groups.append(model_group_info)
|
||||
|
||||
## check for public model groups
|
||||
if litellm.public_model_groups is not None:
|
||||
for mg in model_groups:
|
||||
if mg.model_group in litellm.public_model_groups:
|
||||
mg.is_public_model_group = True
|
||||
|
||||
return model_groups
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model_group/info",
|
||||
tags=["model management"],
|
||||
|
|
@ -6772,7 +6788,7 @@ async def model_group_info(
|
|||
infer_model_from_keys=general_settings.get("infer_model_from_keys", False),
|
||||
llm_router=llm_router,
|
||||
)
|
||||
model_groups: List[ModelGroupInfo] = _get_model_group_info(
|
||||
model_groups: List[ModelGroupInfoProxy] = _get_model_group_info(
|
||||
llm_router=llm_router, all_models_str=all_models_str, model_group=model_group
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.router import ModelGroupInfo
|
||||
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
|
||||
ModelGroupInfoProxy,
|
||||
)
|
||||
from litellm.types.proxy.public_endpoints.public_endpoints import PublicModelHubInfo
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -13,7 +16,7 @@ router = APIRouter()
|
|||
"/public/model_hub",
|
||||
tags=["public", "model management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=List[ModelGroupInfo],
|
||||
response_model=List[ModelGroupInfoProxy],
|
||||
)
|
||||
async def public_model_hub():
|
||||
import litellm
|
||||
|
|
@ -24,7 +27,7 @@ async def public_model_hub():
|
|||
status_code=400, detail=CommonProxyErrors.no_llm_router.value
|
||||
)
|
||||
|
||||
model_groups: List[ModelGroupInfo] = []
|
||||
model_groups: List[ModelGroupInfoProxy] = []
|
||||
if litellm.public_model_groups is not None:
|
||||
model_groups = _get_model_group_info(
|
||||
llm_router=llm_router,
|
||||
|
|
@ -33,3 +36,27 @@ async def public_model_hub():
|
|||
)
|
||||
|
||||
return model_groups
|
||||
|
||||
|
||||
@router.get(
|
||||
"/public/model_hub/info",
|
||||
tags=["public", "model management"],
|
||||
response_model=PublicModelHubInfo,
|
||||
)
|
||||
async def public_model_hub_info():
|
||||
import litellm
|
||||
from litellm.proxy.proxy_server import _title, version
|
||||
|
||||
try:
|
||||
from litellm_enterprise.proxy.proxy_server import EnterpriseProxyConfig
|
||||
|
||||
custom_docs_description = EnterpriseProxyConfig.get_custom_docs_description()
|
||||
except Exception:
|
||||
custom_docs_description = None
|
||||
|
||||
return PublicModelHubInfo(
|
||||
docs_title=_title,
|
||||
custom_docs_description=custom_docs_description,
|
||||
litellm_version=version,
|
||||
useful_links=litellm.public_model_groups_links,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ...router import ModelGroupInfo
|
||||
|
||||
|
||||
class ModelGroupInfoProxy(ModelGroupInfo):
|
||||
is_public_model_group: bool = Field(default=False)
|
||||
10
litellm/types/proxy/public_endpoints/public_endpoints.py
Normal file
10
litellm/types/proxy/public_endpoints/public_endpoints.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PublicModelHubInfo(BaseModel):
|
||||
docs_title: str
|
||||
custom_docs_description: Optional[str]
|
||||
litellm_version: str
|
||||
useful_links: Optional[Dict[str, str]]
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { modelHubCall } from "./networking";
|
||||
import { modelHubCall, makeModelGroupPublic, modelHubPublicModelsCall, proxyBaseUrl } from "./networking";
|
||||
import { getConfigFieldSetting, updateConfigFieldSetting } from "./networking";
|
||||
import { ModelDataTable } from "./model_dashboard/table";
|
||||
import { modelHubColumns } from "./model_hub_table_columns";
|
||||
import PublicModelHub from "./public_model_hub";
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
|
|
@ -55,16 +56,13 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("");
|
||||
const [selectedMode, setSelectedMode] = useState<string>("");
|
||||
const [selectedFeature, setSelectedFeature] = useState<string>("");
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
const tableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
const fetchData = async (accessToken: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const _modelHubData = await modelHubCall(accessToken);
|
||||
|
|
@ -88,7 +86,28 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const fetchPublicData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const _modelHubData = await modelHubPublicModelsCall();
|
||||
console.log("ModelHubData:", _modelHubData);
|
||||
console.log("First model structure:", _modelHubData[0]);
|
||||
console.log("Model has model_group?", _modelHubData[0]?.model_group);
|
||||
console.log("Model has providers?", _modelHubData[0]?.providers);
|
||||
setModelHubData(_modelHubData);
|
||||
setPublicPageAllowed(true);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public model data", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
fetchData(accessToken);
|
||||
} else if (publicPage) {
|
||||
fetchPublicData();
|
||||
}
|
||||
}, [accessToken, publicPage]);
|
||||
|
||||
const showModal = (model: ModelGroupInfo) => {
|
||||
|
|
@ -104,11 +123,30 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
updateConfigFieldSetting(accessToken, "enable_public_model_hub", true).then(
|
||||
(data) => {
|
||||
|
||||
try {
|
||||
// Get the selected model groups or use all if none are selected
|
||||
const modelGroupsToMakePublic = selectedModels.size > 0
|
||||
? Array.from(selectedModels)
|
||||
: modelHubData?.map(model => model.model_group) || [];
|
||||
|
||||
if (modelGroupsToMakePublic.length > 0) {
|
||||
// Call the endpoint to make the selected model groups public
|
||||
await makeModelGroupPublic(accessToken, modelGroupsToMakePublic);
|
||||
|
||||
// Show success message
|
||||
message.success(`Successfully made ${modelGroupsToMakePublic.length} model group(s) public!`);
|
||||
|
||||
// Route to the model hub table
|
||||
router.push(`/ui/model_hub_table`);
|
||||
} else {
|
||||
// Show the modal if no model groups available
|
||||
setIsPublicPageModalVisible(true);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error making model groups public:", error);
|
||||
message.error("Failed to make model groups public. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
|
|
@ -164,11 +202,44 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
return Array.from(modes);
|
||||
};
|
||||
|
||||
const getUniqueFeatures = (data: ModelGroupInfo[]) => {
|
||||
const features = new Set<string>();
|
||||
data.forEach(model => {
|
||||
// Find all properties that start with 'supports_' and are true
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.forEach(([key]) => {
|
||||
// Format the feature name (remove 'supports_' prefix and convert to title case)
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
features.add(featureName);
|
||||
});
|
||||
});
|
||||
return Array.from(features).sort();
|
||||
};
|
||||
|
||||
const filteredData = modelHubData?.filter(model => {
|
||||
const matchesSearch = model.model_group.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesProvider = selectedProvider === "" || model.providers.includes(selectedProvider);
|
||||
const matchesMode = selectedMode === "" || model.mode === selectedMode;
|
||||
return matchesSearch && matchesProvider && matchesMode;
|
||||
|
||||
// Check if model has the selected feature
|
||||
const matchesFeature = selectedFeature === "" ||
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.some(([key]) => {
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return featureName === selectedFeature;
|
||||
});
|
||||
|
||||
return matchesSearch && matchesProvider && matchesMode && matchesFeature;
|
||||
}) || [];
|
||||
|
||||
const handleRowSelection = (modelGroup: string, isSelected: boolean) => {
|
||||
|
|
@ -198,17 +269,33 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
// Clear selections when filters change to avoid confusion
|
||||
useEffect(() => {
|
||||
setSelectedModels(new Set());
|
||||
}, [searchTerm, selectedProvider, selectedMode]);
|
||||
}, [searchTerm, selectedProvider, selectedMode, selectedFeature]);
|
||||
|
||||
console.log("publicPage: ", publicPage);
|
||||
console.log("publicPageAllowed: ", publicPageAllowed);
|
||||
|
||||
// If this is a public page, use the dedicated PublicModelHub component
|
||||
if (publicPage && publicPageAllowed) {
|
||||
return <PublicModelHub accessToken={accessToken} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-4 h-[75vh]">
|
||||
{(publicPage && publicPageAllowed) || publicPage == false ? (
|
||||
{publicPage == false ? (
|
||||
<div className="w-full m-2 mt-2 p-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<Title className="text-center">Model Hub - Table View</Title>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Text>Model Hub URL:</Text>
|
||||
<Text className="bg-gray-200 px-2 py-1 rounded">{`${proxyBaseUrl}/ui/model_hub_table`}</Text>
|
||||
|
||||
{publicPage == false ? (
|
||||
premiumUser ? (
|
||||
<Button className="ml-4" onClick={() => handleMakePublicPage()}>
|
||||
<Button
|
||||
className="ml-4"
|
||||
onClick={() => handleMakePublicPage()}
|
||||
disabled={selectedModels.size === 0}
|
||||
>
|
||||
✨ Make Public
|
||||
</Button>
|
||||
) : (
|
||||
|
|
@ -225,6 +312,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="mb-6">
|
||||
|
|
@ -265,6 +353,19 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2">Features:</Text>
|
||||
<select
|
||||
value={selectedFeature}
|
||||
onChange={(e) => setSelectedFeature(e.target.value)}
|
||||
className="border rounded px-3 py-2 text-sm text-gray-600 w-48 h-10"
|
||||
>
|
||||
<option value="" className="text-sm text-gray-600">All Features</option>
|
||||
{modelHubData && getUniqueFeatures(modelHubData).map(feature => (
|
||||
<option key={feature} value={feature} className="text-sm text-gray-800">{feature}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
|
@ -278,6 +379,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
handleSelectAll,
|
||||
showModal,
|
||||
copyToClipboard,
|
||||
publicPage,
|
||||
)}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
|
|
@ -330,7 +432,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
|
|||
<div className="flex justify-between mb-4">
|
||||
<Text className="text-base mr-2">Shareable Link:</Text>
|
||||
<Text className="max-w-sm ml-2 bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center rounded">
|
||||
{`<proxy_base_url>/ui/model_hub_table?key=<YOUR_API_KEY>`}
|
||||
{`${proxyBaseUrl}/model_hub_table`}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ export const modelHubColumns = (
|
|||
handleSelectAll: (checked: boolean) => void,
|
||||
showModal: (model: ModelHubData) => void,
|
||||
copyToClipboard: (text: string) => void,
|
||||
): ColumnDef<ModelHubData>[] => [
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<ModelHubData>[] => {
|
||||
const allColumns: ColumnDef<ModelHubData>[] = [
|
||||
{
|
||||
header: () => (
|
||||
<Checkbox
|
||||
|
|
@ -241,14 +243,14 @@ export const modelHubColumns = (
|
|||
accessorKey: "public",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const publicA = rowA.original.public === true ? 1 : 0;
|
||||
const publicB = rowB.original.public === true ? 1 : 0;
|
||||
const publicA = rowA.original.is_public_model_group === true ? 1 : 0;
|
||||
const publicB = rowB.original.is_public_model_group === true ? 1 : 0;
|
||||
return publicA - publicB;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return model.public === true ? (
|
||||
return model.is_public_model_group === true ? (
|
||||
<Badge color="green" size="xs">Yes</Badge>
|
||||
) : (
|
||||
<Badge color="gray" size="xs">No</Badge>
|
||||
|
|
@ -278,4 +280,20 @@ export const modelHubColumns = (
|
|||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
// Filter out columns based on publicPage setting
|
||||
if (publicPage) {
|
||||
return allColumns.filter(column => {
|
||||
// Remove the select/checkbox column
|
||||
if (column.id === "select") return false;
|
||||
|
||||
// Remove the public column
|
||||
if ('accessorKey' in column && column.accessorKey === "public") return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return allColumns;
|
||||
};
|
||||
|
|
@ -108,6 +108,13 @@ export interface CredentialItem {
|
|||
};
|
||||
}
|
||||
|
||||
export interface PublicModelHubInfo {
|
||||
docs_title: string;
|
||||
custom_docs_description: string | null;
|
||||
litellm_version: string;
|
||||
useful_links: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface LiteLLMWellKnownUiConfig {
|
||||
server_root_path: string;
|
||||
proxy_base_url: string | null;
|
||||
|
|
@ -148,6 +155,21 @@ export function setGlobalLitellmHeaderName(
|
|||
globalLitellmHeaderName = headerName;
|
||||
}
|
||||
|
||||
export const makeModelGroupPublic = async (accessToken: string, modelGroups: string[]) => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/model_group/make_public` : `/model_group/make_public`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model_groups: modelGroups,
|
||||
}),
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getUiConfig = async () => {
|
||||
console.log("Getting UI config");
|
||||
/**Special route to get the proxy base url and server root path */
|
||||
|
|
@ -164,6 +186,15 @@ export const getUiConfig = async () => {
|
|||
return jsonData;
|
||||
};
|
||||
|
||||
export const getPublicModelHubInfo = async () => {
|
||||
const url = defaultProxyBaseUrl
|
||||
? `${defaultProxyBaseUrl}/public/model_hub/info`
|
||||
: `/public/model_hub/info`;
|
||||
const response = await fetch(url);
|
||||
const jsonData: PublicModelHubInfo = await response.json();
|
||||
return jsonData;
|
||||
};
|
||||
|
||||
export const getOpenAPISchema = async () => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/openapi.json` : `/openapi.json`;
|
||||
const response = await fetch(url);
|
||||
|
|
@ -1769,6 +1800,17 @@ export const modelInfoV1Call = async (accessToken: String, modelId: String) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const modelHubPublicModelsCall = async () => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/public/model_hub` : `/public/model_hub`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const modelHubCall = async (accessToken: String) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
|
|
@ -4577,6 +4619,7 @@ export const healthCheckHistoryCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const latestHealthChecksCall = async (accessToken: String) => {
|
||||
/**
|
||||
* Get the latest health check status for all models
|
||||
|
|
|
|||
870
ui/litellm-dashboard/src/components/public_model_hub.tsx
Normal file
870
ui/litellm-dashboard/src/components/public_model_hub.tsx
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
import React, { useEffect, useState, useRef, useMemo } from "react";
|
||||
import { modelHubPublicModelsCall, proxyBaseUrl, getUiConfig, getPublicModelHubInfo } from "./networking";
|
||||
import { ModelDataTable } from "./model_dashboard/table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
Button,
|
||||
} from "@tremor/react";
|
||||
import { message, Tag, Tooltip, Modal, Select } from "antd";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { ExternalLinkIcon, SearchIcon, EyeIcon, CogIcon } from "@heroicons/react/outline";
|
||||
import { Copy, Info } from "lucide-react";
|
||||
import { Table as TableInstance } from '@tanstack/react-table';
|
||||
import { generateCodeSnippet } from "./chat_ui/CodeSnippets";
|
||||
import { EndpointType, getEndpointType } from "./chat_ui/mode_endpoint_mapping";
|
||||
import { MessageType } from "./chat_ui/types";
|
||||
import { getProviderLogoAndName } from "./provider_info_helpers";
|
||||
// Simple approach without react-markdown dependency
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
providers: string[];
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
input_cost_per_token?: number;
|
||||
output_cost_per_token?: number;
|
||||
mode?: string;
|
||||
tpm?: number;
|
||||
rpm?: number;
|
||||
supports_parallel_function_calling: boolean;
|
||||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface PublicModelHubProps {
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken }) => {
|
||||
const [modelHubData, setModelHubData] = useState<ModelGroupInfo[] | null>(null);
|
||||
const [pageTitle, setPageTitle] = useState<string>("LiteLLM Gateway");
|
||||
const [customDocsDescription, setCustomDocsDescription] = useState<string | null>(null);
|
||||
const [litellmVersion, setLitellmVersion] = useState<string>("");
|
||||
const [usefulLinks, setUsefulLinks] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
|
||||
const [selectedModes, setSelectedModes] = useState<string[]>([]);
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);
|
||||
const [serviceStatus, setServiceStatus] = useState<string>("I'm alive! ✓");
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<null | ModelGroupInfo>(null);
|
||||
const tableRef = useRef<TableInstance<any>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPublicData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const _modelHubData = await modelHubPublicModelsCall();
|
||||
console.log("ModelHubData:", _modelHubData);
|
||||
setModelHubData(_modelHubData);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public model data", error);
|
||||
setServiceStatus("Service unavailable");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPublicModelHubInfo = async () => {
|
||||
const publicModelHubInfo = await getPublicModelHubInfo();
|
||||
console.log("Public Model Hub Info:", publicModelHubInfo);
|
||||
setPageTitle(publicModelHubInfo.docs_title);
|
||||
setCustomDocsDescription(publicModelHubInfo.custom_docs_description);
|
||||
setLitellmVersion(publicModelHubInfo.litellm_version);
|
||||
setUsefulLinks(publicModelHubInfo.useful_links || {});
|
||||
};
|
||||
|
||||
|
||||
fetchPublicModelHubInfo();
|
||||
|
||||
fetchPublicData();
|
||||
}, []);
|
||||
|
||||
// Clear filters when filter values change to avoid confusion
|
||||
useEffect(() => {
|
||||
// This would clear selections if we had any selection functionality
|
||||
// For now, it's just for consistency with the original component
|
||||
}, [searchTerm, selectedProviders, selectedModes, selectedFeatures]);
|
||||
|
||||
const getUniqueProviders = (data: ModelGroupInfo[]) => {
|
||||
const providers = new Set<string>();
|
||||
data.forEach(model => {
|
||||
model.providers.forEach(provider => providers.add(provider));
|
||||
});
|
||||
return Array.from(providers);
|
||||
};
|
||||
|
||||
const getUniqueModes = (data: ModelGroupInfo[]) => {
|
||||
const modes = new Set<string>();
|
||||
data.forEach(model => {
|
||||
if (model.mode) modes.add(model.mode);
|
||||
});
|
||||
return Array.from(modes);
|
||||
};
|
||||
|
||||
const getUniqueFeatures = (data: ModelGroupInfo[]) => {
|
||||
const features = new Set<string>();
|
||||
data.forEach(model => {
|
||||
// Find all properties that start with 'supports_' and are true
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.forEach(([key]) => {
|
||||
// Format the feature name (remove 'supports_' prefix and convert to title case)
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
features.add(featureName);
|
||||
});
|
||||
});
|
||||
return Array.from(features).sort();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!modelHubData) return [];
|
||||
|
||||
let searchResults = modelHubData;
|
||||
|
||||
// Apply search if there's a search term
|
||||
if (searchTerm.trim()) {
|
||||
const lowercaseSearch = searchTerm.toLowerCase();
|
||||
const searchWords = lowercaseSearch.split(/\s+/);
|
||||
|
||||
// First, try flexible matching that handles different separators
|
||||
const exactMatches = modelHubData.filter(model => {
|
||||
const modelName = model.model_group.toLowerCase();
|
||||
|
||||
// Check if it contains the exact search term
|
||||
if (modelName.includes(lowercaseSearch)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it contains all search words (handles spaces vs slashes/dashes)
|
||||
return searchWords.every(word => modelName.includes(word));
|
||||
});
|
||||
|
||||
// If we have exact matches, rank them by relevance
|
||||
if (exactMatches.length > 0) {
|
||||
searchResults = exactMatches.sort((a, b) => {
|
||||
const aName = a.model_group.toLowerCase();
|
||||
const bName = b.model_group.toLowerCase();
|
||||
|
||||
// Calculate relevance scores
|
||||
const aExactMatch = aName === lowercaseSearch ? 1000 : 0;
|
||||
const bExactMatch = bName === lowercaseSearch ? 1000 : 0;
|
||||
|
||||
const aStartsWith = aName.startsWith(lowercaseSearch) ? 100 : 0;
|
||||
const bStartsWith = bName.startsWith(lowercaseSearch) ? 100 : 0;
|
||||
|
||||
const aContainsWords = lowercaseSearch.split(/\s+/).every(word => aName.includes(word)) ? 50 : 0;
|
||||
const bContainsWords = lowercaseSearch.split(/\s+/).every(word => bName.includes(word)) ? 50 : 0;
|
||||
|
||||
const aLength = aName.length;
|
||||
const bLength = bName.length;
|
||||
|
||||
const aScore = aExactMatch + aStartsWith + aContainsWords + (1000 - aLength);
|
||||
const bScore = bExactMatch + bStartsWith + bContainsWords + (1000 - bLength);
|
||||
|
||||
return bScore - aScore; // Higher score first
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply other filters
|
||||
return searchResults.filter(model => {
|
||||
const matchesProvider = selectedProviders.length === 0 || selectedProviders.some(provider => model.providers.includes(provider));
|
||||
const matchesMode = selectedModes.length === 0 || selectedModes.includes(model.mode || "");
|
||||
|
||||
// Check if model has any of the selected features
|
||||
const matchesFeature = selectedFeatures.length === 0 ||
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.some(([key]) => {
|
||||
const featureName = key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return selectedFeatures.includes(featureName);
|
||||
});
|
||||
|
||||
return matchesProvider && matchesMode && matchesFeature;
|
||||
});
|
||||
}, [modelHubData, searchTerm, selectedProviders, selectedModes, selectedFeatures]);
|
||||
|
||||
|
||||
|
||||
const showModal = (model: ModelGroupInfo) => {
|
||||
setSelectedModel(model);
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalOk = () => {
|
||||
setIsModalVisible(false);
|
||||
setSelectedModel(null);
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
setSelectedModel(null);
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
message.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
const formatCapabilityName = (key: string) => {
|
||||
return key
|
||||
.replace(/^supports_/, '')
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const getModelCapabilities = (model: ModelGroupInfo) => {
|
||||
return Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const formatCost = (cost: number) => {
|
||||
return `$${(cost * 1_000_000).toFixed(4)}`;
|
||||
};
|
||||
|
||||
const formatTokens = (tokens: number | undefined) => {
|
||||
if (!tokens) return "N/A";
|
||||
if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(0)}K`;
|
||||
}
|
||||
return tokens.toString();
|
||||
};
|
||||
|
||||
const formatLimits = (rpm?: number, tpm?: number) => {
|
||||
const limits = [];
|
||||
if (rpm) limits.push(`RPM: ${rpm.toLocaleString()}`);
|
||||
if (tpm) limits.push(`TPM: ${tpm.toLocaleString()}`);
|
||||
return limits.length > 0 ? limits.join(", ") : "N/A";
|
||||
};
|
||||
|
||||
|
||||
|
||||
const publicModelHubColumns = (): ColumnDef<ModelGroupInfo>[] => [
|
||||
{
|
||||
header: "Model Name",
|
||||
accessorKey: "model_group",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={row.original.model_group}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left"
|
||||
onClick={() => showModal(row.original)}
|
||||
>
|
||||
{row.original.model_group}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Providers",
|
||||
accessorKey: "providers",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const providers = row.original.providers;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{providers.map((provider) => {
|
||||
const { logo } = getProviderLogoAndName(provider);
|
||||
return (
|
||||
<div
|
||||
key={provider}
|
||||
className="flex items-center space-x-1 px-2 py-1 bg-gray-100 rounded text-xs"
|
||||
>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={provider}
|
||||
className="w-3 h-3 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{provider}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Mode",
|
||||
accessorKey: "mode",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const mode = row.original.mode;
|
||||
const getModeIcon = (mode: string) => {
|
||||
switch (mode?.toLowerCase()) {
|
||||
case "chat":
|
||||
return "💬";
|
||||
case "rerank":
|
||||
return "🔄";
|
||||
case "embedding":
|
||||
return "📄";
|
||||
default:
|
||||
return "🤖";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{getModeIcon(mode || "")}</span>
|
||||
<Text>{mode || "Chat"}</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Max Input",
|
||||
accessorKey: "max_input_tokens",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Text className="text-center">{formatTokens(row.original.max_input_tokens)}</Text>
|
||||
),
|
||||
size: 100,
|
||||
meta: {
|
||||
className: "text-center",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Max Output",
|
||||
accessorKey: "max_output_tokens",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Text className="text-center">{formatTokens(row.original.max_output_tokens)}</Text>
|
||||
),
|
||||
size: 100,
|
||||
meta: {
|
||||
className: "text-center",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Input $/1M",
|
||||
accessorKey: "input_cost_per_token",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const cost = row.original.input_cost_per_token;
|
||||
return (
|
||||
<Text className="text-center">
|
||||
{cost ? formatCost(cost) : "Free"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
meta: {
|
||||
className: "text-center",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Output $/1M",
|
||||
accessorKey: "output_cost_per_token",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const cost = row.original.output_cost_per_token;
|
||||
return (
|
||||
<Text className="text-center">
|
||||
{cost ? formatCost(cost) : "Free"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
meta: {
|
||||
className: "text-center",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Features",
|
||||
accessorKey: "supports_vision",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
// Dynamically get all features that start with 'supports_' and are true
|
||||
const features = Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith('supports_') && value === true)
|
||||
.map(([key]) => formatCapabilityName(key));
|
||||
|
||||
if (features.length === 0) {
|
||||
return <Text className="text-gray-400">-</Text>;
|
||||
}
|
||||
|
||||
if (features.length === 1) {
|
||||
return (
|
||||
<div className="h-6 flex items-center">
|
||||
<Tag color="blue" className="text-xs">
|
||||
{features[0]}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-6 flex items-center space-x-1">
|
||||
<Tag color="blue" className="text-xs">
|
||||
{features[0]}
|
||||
</Tag>
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">All Features:</div>
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="text-xs">• {feature}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
placement="topLeft"
|
||||
>
|
||||
<span
|
||||
className="text-xs text-blue-600 cursor-pointer hover:text-blue-800 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
+{features.length - 1}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Limits",
|
||||
accessorKey: "rpm",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
return (
|
||||
<Text className="text-xs text-gray-600">
|
||||
{formatLimits(model.rpm, model.tpm)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-green-600 text-white px-8 py-6">
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<Title className="text-white text-2xl font-semibold">{pageTitle}</Title>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full px-8 py-12">
|
||||
{/* About Section */}
|
||||
<Card className="mb-10 p-8">
|
||||
<Title className="text-3xl font-semibold mb-6">About</Title>
|
||||
<p className="text-gray-700 mb-6 text-lg leading-relaxed">{customDocsDescription ? customDocsDescription : "Proxy Server to call 100+ LLMs in the OpenAI format."}</p>
|
||||
<div className="flex items-center space-x-3 text-base text-gray-600">
|
||||
<span className="flex items-center">
|
||||
<span className="w-5 h-5 mr-2">🔧</span>
|
||||
Built with litellm: v{litellmVersion}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Useful Links */}
|
||||
{usefulLinks && Object.keys(usefulLinks).length > 0 && (
|
||||
<Card className="mb-10 p-8">
|
||||
<Title className="text-3xl font-semibold mb-6">Useful Links</Title>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{Object.entries(usefulLinks || {}).map(([title, url]) => (
|
||||
<button
|
||||
key={title}
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
className="flex items-center space-x-3 text-blue-600 hover:text-blue-800 transition-colors p-4 rounded-lg hover:bg-blue-50"
|
||||
>
|
||||
<ExternalLinkIcon className="w-5 h-5" />
|
||||
<Text className="text-base font-medium">{title}</Text>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Health and Endpoint Status */}
|
||||
<Card className="mb-10 p-8">
|
||||
<Title className="text-3xl font-semibold mb-6">Health and Endpoint Status</Title>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<Text className="text-green-600 font-medium text-base">Service status: {serviceStatus}</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="mb-10 p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Text className="text-base font-medium">Search Models:</Text>
|
||||
<Tooltip title="Smart search with relevance ranking - finds models containing your search terms, ranked by relevance. Try searching 'xai grok-4', 'claude-4', 'gpt-4', or 'sonnet'" placement="top">
|
||||
<Info className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<SearchIcon className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search model names... (smart search enabled)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border rounded-lg pl-10 pr-4 py-3 w-full text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-base font-medium mb-3">Provider:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedProviders}
|
||||
onChange={(values) => setSelectedProviders(values)}
|
||||
placeholder="Select providers"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
optionRender={(option) => {
|
||||
const { logo } = getProviderLogoAndName(option.value as string);
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={option.label as string}
|
||||
className="w-5 h-5 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{option.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{modelHubData && getUniqueProviders(modelHubData).map(provider => (
|
||||
<Select.Option key={provider} value={provider}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getProviderLogoAndName(provider).logo && (
|
||||
<img
|
||||
src={getProviderLogoAndName(provider).logo}
|
||||
alt={provider}
|
||||
className="w-5 h-5 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{provider}</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-base font-medium mb-3">Mode:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedModes}
|
||||
onChange={(values) => setSelectedModes(values)}
|
||||
placeholder="Select modes"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData && getUniqueModes(modelHubData).map(mode => (
|
||||
<Select.Option key={mode} value={mode}>{mode}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-base font-medium mb-3">Features:</Text>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedFeatures}
|
||||
onChange={(values) => setSelectedFeatures(values)}
|
||||
placeholder="Select features"
|
||||
className="w-full"
|
||||
size="large"
|
||||
allowClear
|
||||
>
|
||||
{modelHubData && getUniqueFeatures(modelHubData).map(feature => (
|
||||
<Select.Option key={feature} value={feature}>{feature}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Models Table */}
|
||||
<Card className="p-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-3xl font-semibold">Available Models</Title>
|
||||
</div>
|
||||
|
||||
<ModelDataTable
|
||||
columns={publicModelHubColumns()}
|
||||
data={filteredData}
|
||||
isLoading={loading}
|
||||
table={tableRef}
|
||||
defaultSorting={[{ id: "model_group", desc: false }]}
|
||||
/>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Text className="text-base text-gray-600">
|
||||
Showing {filteredData.length} of {modelHubData?.length || 0} models
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Model Details Modal */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{selectedModel?.model_group || "Model Details"}</span>
|
||||
{selectedModel && (
|
||||
<Tooltip title="Copy model name">
|
||||
<Copy
|
||||
onClick={() => copyToClipboard(selectedModel.model_group)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width={1000}
|
||||
open={isModalVisible}
|
||||
footer={null}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
>
|
||||
{selectedModel && (
|
||||
<div className="space-y-6">
|
||||
{/* Model Overview */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Model Overview</Text>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<Text className="font-medium">Model Name:</Text>
|
||||
<Text>{selectedModel.model_group}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Mode:</Text>
|
||||
<Text>{selectedModel.mode || "Not specified"}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Providers:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedModel.providers.map(provider => {
|
||||
const { logo } = getProviderLogoAndName(provider);
|
||||
return (
|
||||
<Tag key={provider} color="blue">
|
||||
<div className="flex items-center space-x-1">
|
||||
{logo && (
|
||||
<img
|
||||
src={logo}
|
||||
alt={provider}
|
||||
className="w-3 h-3 flex-shrink-0 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="capitalize">{provider}</span>
|
||||
</div>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wildcard Routing Note */}
|
||||
{selectedModel.model_group.includes('*') && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Info className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<Text className="font-medium text-blue-900 mb-2">Wildcard Routing</Text>
|
||||
<Text className="text-sm text-blue-800 mb-2">
|
||||
This model uses wildcard routing. You can pass any value where you see the <code className="bg-blue-100 px-1 py-0.5 rounded text-xs">*</code> symbol.
|
||||
</Text>
|
||||
<Text className="text-sm text-blue-800">
|
||||
For example, with <code className="bg-blue-100 px-1 py-0.5 rounded text-xs">{selectedModel.model_group}</code>, you can use any string (<code className="bg-blue-100 px-1 py-0.5 rounded text-xs">{selectedModel.model_group.replace('*', 'my-custom-value')}</code>) that matches this pattern.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token and Cost Information */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Token & Cost Information</Text>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Text className="font-medium">Max Input Tokens:</Text>
|
||||
<Text>{selectedModel.max_input_tokens?.toLocaleString() || "Not specified"}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Max Output Tokens:</Text>
|
||||
<Text>{selectedModel.max_output_tokens?.toLocaleString() || "Not specified"}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Input Cost per 1M Tokens:</Text>
|
||||
<Text>{selectedModel.input_cost_per_token ? formatCost(selectedModel.input_cost_per_token) : "Not specified"}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Output Cost per 1M Tokens:</Text>
|
||||
<Text>{selectedModel.output_cost_per_token ? formatCost(selectedModel.output_cost_per_token) : "Not specified"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Capabilities</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(() => {
|
||||
const capabilities = getModelCapabilities(selectedModel);
|
||||
const colors = ['green', 'blue', 'purple', 'orange', 'red', 'yellow'];
|
||||
|
||||
if (capabilities.length === 0) {
|
||||
return <Text className="text-gray-500">No special capabilities listed</Text>;
|
||||
}
|
||||
|
||||
return capabilities.map((capability, index) => (
|
||||
<Tag
|
||||
key={capability}
|
||||
color={colors[index % colors.length]}
|
||||
>
|
||||
{formatCapabilityName(capability)}
|
||||
</Tag>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rate Limits */}
|
||||
{(selectedModel.tpm || selectedModel.rpm) && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Rate Limits</Text>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{selectedModel.tpm && (
|
||||
<div>
|
||||
<Text className="font-medium">Tokens per Minute:</Text>
|
||||
<Text>{selectedModel.tpm.toLocaleString()}</Text>
|
||||
</div>
|
||||
)}
|
||||
{selectedModel.rpm && (
|
||||
<div>
|
||||
<Text className="font-medium">Requests per Minute:</Text>
|
||||
<Text>{selectedModel.rpm.toLocaleString()}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Supported OpenAI Parameters */}
|
||||
{selectedModel.supported_openai_params && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Supported OpenAI Parameters</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedModel.supported_openai_params.map(param => (
|
||||
<Tag key={param} color="green">{param}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage Example */}
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Usage Example</Text>
|
||||
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
||||
<pre className="text-sm">
|
||||
{(() => {
|
||||
const codeSnippet = generateCodeSnippet({
|
||||
apiKeySource: 'custom',
|
||||
accessToken: null,
|
||||
apiKey: 'your_api_key',
|
||||
inputMessage: 'Hello, how are you?',
|
||||
chatHistory: [
|
||||
{ role: 'user', content: 'Hello, how are you?', isImage: false } as MessageType
|
||||
],
|
||||
selectedTags: [],
|
||||
selectedVectorStores: [],
|
||||
selectedGuardrails: [],
|
||||
endpointType: getEndpointType(selectedModel.mode || 'chat'),
|
||||
selectedModel: selectedModel.model_group,
|
||||
selectedSdk: 'openai'
|
||||
});
|
||||
return codeSnippet;
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
const codeSnippet = generateCodeSnippet({
|
||||
apiKeySource: 'custom',
|
||||
accessToken: null,
|
||||
apiKey: 'your_api_key',
|
||||
inputMessage: 'Hello, how are you?',
|
||||
chatHistory: [
|
||||
{ role: 'user', content: 'Hello, how are you?', isImage: false } as MessageType
|
||||
],
|
||||
selectedTags: [],
|
||||
selectedVectorStores: [],
|
||||
selectedGuardrails: [],
|
||||
endpointType: getEndpointType(selectedModel.mode || 'chat'),
|
||||
selectedModel: selectedModel.model_group,
|
||||
selectedSdk: 'openai'
|
||||
});
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicModelHub;
|
||||
218
ui/litellm-dashboard/src/components/public_model_hub_columns.tsx
Normal file
218
ui/litellm-dashboard/src/components/public_model_hub_columns.tsx
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Text } from "@tremor/react";
|
||||
import { EyeIcon, CogIcon } from "@heroicons/react/outline";
|
||||
import { Tag } from "antd";
|
||||
|
||||
interface ModelGroupInfo {
|
||||
model_group: string;
|
||||
providers: string[];
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
input_cost_per_token?: number;
|
||||
output_cost_per_token?: number;
|
||||
mode?: string;
|
||||
tpm?: number;
|
||||
rpm?: number;
|
||||
supports_parallel_function_calling: boolean;
|
||||
supports_vision: boolean;
|
||||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const formatCost = (cost: number) => {
|
||||
return `$${(cost * 1_000_000).toFixed(4)}`;
|
||||
};
|
||||
|
||||
const formatTokens = (tokens: number | undefined) => {
|
||||
if (!tokens) return "N/A";
|
||||
if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(0)}K`;
|
||||
}
|
||||
return tokens.toString();
|
||||
};
|
||||
|
||||
const formatLimits = (rpm?: number, tpm?: number) => {
|
||||
const limits = [];
|
||||
if (rpm) limits.push(`RPM: ${rpm.toLocaleString()}`);
|
||||
if (tpm) limits.push(`TPM: ${tpm.toLocaleString()}`);
|
||||
return limits.length > 0 ? limits.join(", ") : "N/A";
|
||||
};
|
||||
|
||||
export const publicModelHubColumns = (): ColumnDef<ModelGroupInfo>[] => [
|
||||
{
|
||||
header: "#",
|
||||
id: "index",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const index = row.index + 1;
|
||||
return <Text className="text-center">{index}</Text>;
|
||||
},
|
||||
size: 50,
|
||||
},
|
||||
{
|
||||
header: "Model Group",
|
||||
accessorKey: "model_group",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Text className="font-medium">{row.original.model_group}</Text>
|
||||
),
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "Providers",
|
||||
accessorKey: "providers",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const providers = row.original.providers;
|
||||
const getProviderColor = (provider: string) => {
|
||||
switch (provider.toLowerCase()) {
|
||||
case "openai":
|
||||
return "green";
|
||||
case "anthropic":
|
||||
return "orange";
|
||||
case "cohere":
|
||||
return "blue";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{providers.map((provider) => (
|
||||
<Tag
|
||||
key={provider}
|
||||
color={getProviderColor(provider)}
|
||||
className="text-xs"
|
||||
>
|
||||
{provider}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: "Mode",
|
||||
accessorKey: "mode",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const mode = row.original.mode;
|
||||
const getModeIcon = (mode: string) => {
|
||||
switch (mode?.toLowerCase()) {
|
||||
case "chat":
|
||||
return "💬";
|
||||
case "rerank":
|
||||
return "🔄";
|
||||
case "embedding":
|
||||
return "📄";
|
||||
default:
|
||||
return "🤖";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{getModeIcon(mode || "")}</span>
|
||||
<Text>{mode || "Chat"}</Text>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Max Input",
|
||||
accessorKey: "max_input_tokens",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Text className="text-center">{formatTokens(row.original.max_input_tokens)}</Text>
|
||||
),
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Max Output",
|
||||
accessorKey: "max_output_tokens",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<Text className="text-center">{formatTokens(row.original.max_output_tokens)}</Text>
|
||||
),
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Input $/1K",
|
||||
accessorKey: "input_cost_per_token",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const cost = row.original.input_cost_per_token;
|
||||
return (
|
||||
<Text className="text-center">
|
||||
{cost ? formatCost(cost) : "Free"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Output $/1K",
|
||||
accessorKey: "output_cost_per_token",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const cost = row.original.output_cost_per_token;
|
||||
return (
|
||||
<Text className="text-center">
|
||||
{cost ? formatCost(cost) : "Free"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Features",
|
||||
accessorKey: "supports_vision",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const features = [];
|
||||
|
||||
if (model.supports_vision) {
|
||||
features.push(
|
||||
<div key="vision" className="flex items-center space-x-1" title="Vision">
|
||||
<EyeIcon className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (model.supports_function_calling || model.supports_parallel_function_calling) {
|
||||
features.push(
|
||||
<div key="functions" className="flex items-center space-x-1" title="Functions">
|
||||
<CogIcon className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return features.length > 0 ? (
|
||||
<div className="flex space-x-2">{features}</div>
|
||||
) : (
|
||||
<Text className="text-gray-400">-</Text>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "Limits",
|
||||
accessorKey: "rpm",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
return (
|
||||
<Text className="text-xs text-gray-600">
|
||||
{formatLimits(model.rpm, model.tpm)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Reference in a new issue