build(ui/): UI - Public Model Hub v2 (#12532)

Closes https://github.com/BerriAI/litellm/pull/12532
This commit is contained in:
Krrish Dholakia 2025-07-11 22:24:31 -07:00
parent 75d68a47d0
commit 8b4673fe84
15 changed files with 1522 additions and 60 deletions

View file

@ -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

View file

@ -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

View file

@ -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(),
)

View file

@ -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.

View file

@ -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
)

View file

@ -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,
)

View file

@ -0,0 +1,7 @@
from pydantic import Field
from ...router import ModelGroupInfo
class ModelGroupInfoProxy(ModelGroupInfo):
is_public_model_group: bool = Field(default=False)

View 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]]

View file

@ -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">

View file

@ -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;
};

View file

@ -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

View 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;

View 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,
},
];