diff --git a/cookbook/ai_coding_tool_guides/index.json b/cookbook/ai_coding_tool_guides/index.json
index 7d022d6de3..f879292aef 100644
--- a/cookbook/ai_coding_tool_guides/index.json
+++ b/cookbook/ai_coding_tool_guides/index.json
@@ -95,4 +95,40 @@
"LiteLLM",
"Quickstart"
]
+},
+{
+ "title": "AI Coding Tool Usage Tracking",
+ "description": "This is a guide to tracking usage for AI coding tools monitor the use of Claude Code , Google Antigravity, OpenAI Codex, Roo Code etc. through LiteLLM.",
+ "url": "https://docs.litellm.ai/docs/tutorials/cost_tracking_coding",
+ "date": "2026-01-17",
+ "version": "1.0.0",
+ "tags": [
+ "Claude Code",
+ "Gemini CLI",
+ "OpenAI Codex",
+ "LiteLLM"
+ ]
+},
+{
+ "title": "Use Web Search with Claude Code (across OpenAI/Anthropic/Gemini/etc.)",
+ "description": "This is a guide for using Web Search with Claude Code via LiteLLM.",
+ "url": "https://docs.litellm.ai/docs/tutorials/claude_code_websearch",
+ "date": "2026-01-17",
+ "version": "1.0.0",
+ "tags": [
+ "Claude Code",
+ "LiteLLM",
+ "Web Search"
+ ]
+},
+{
+ "title": "Track Claude Code Usage per user via Custom Headers",
+ "description": "This is a guide for tracking claude code user usage by passing a customer ID header.",
+ "url": "https://docs.litellm.ai/docs/tutorials/claude_code_customer_tracking",
+ "date": "2026-01-17",
+ "version": "1.0.0",
+ "tags": [
+ "Claude Code",
+ "LiteLLM"
+ ]
}]
\ No newline at end of file
diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120021_baseline_diff/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120021_baseline_diff/migration.sql
deleted file mode 100644
index 2f725d8380..0000000000
--- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120021_baseline_diff/migration.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- This is an empty migration.
-
diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120539_baseline_diff/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120539_baseline_diff/migration.sql
deleted file mode 100644
index 2f725d8380..0000000000
--- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120539_baseline_diff/migration.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- This is an empty migration.
-
diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html
deleted file mode 100644
index c6035eb40c..0000000000
--- a/litellm/proxy/_experimental/out/404.html
+++ /dev/null
@@ -1 +0,0 @@
-
404: This page could not be found.LiteLLM Dashboard404
This page could not be found.
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/api-reference.html b/litellm/proxy/_experimental/out/api-reference/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/api-reference.html
rename to litellm/proxy/_experimental/out/api-reference/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/api-playground.html b/litellm/proxy/_experimental/out/experimental/api-playground/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/api-playground.html
rename to litellm/proxy/_experimental/out/experimental/api-playground/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/budgets.html b/litellm/proxy/_experimental/out/experimental/budgets/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/budgets.html
rename to litellm/proxy/_experimental/out/experimental/budgets/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/caching.html b/litellm/proxy/_experimental/out/experimental/caching/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/caching.html
rename to litellm/proxy/_experimental/out/experimental/caching/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/old-usage.html b/litellm/proxy/_experimental/out/experimental/old-usage/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/old-usage.html
rename to litellm/proxy/_experimental/out/experimental/old-usage/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/prompts.html b/litellm/proxy/_experimental/out/experimental/prompts/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/prompts.html
rename to litellm/proxy/_experimental/out/experimental/prompts/index.html
diff --git a/litellm/proxy/_experimental/out/experimental/tag-management.html b/litellm/proxy/_experimental/out/experimental/tag-management/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/experimental/tag-management.html
rename to litellm/proxy/_experimental/out/experimental/tag-management/index.html
diff --git a/litellm/proxy/_experimental/out/guardrails.html b/litellm/proxy/_experimental/out/guardrails.html
deleted file mode 100644
index 6dae881864..0000000000
--- a/litellm/proxy/_experimental/out/guardrails.html
+++ /dev/null
@@ -1 +0,0 @@
-LiteLLM Dashboard
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/login.html b/litellm/proxy/_experimental/out/login/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/login.html
rename to litellm/proxy/_experimental/out/login/index.html
diff --git a/litellm/proxy/_experimental/out/logs.html b/litellm/proxy/_experimental/out/logs/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/logs.html
rename to litellm/proxy/_experimental/out/logs/index.html
diff --git a/litellm/proxy/_experimental/out/mcp/oauth/callback.html b/litellm/proxy/_experimental/out/mcp/oauth/callback/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/mcp/oauth/callback.html
rename to litellm/proxy/_experimental/out/mcp/oauth/callback/index.html
diff --git a/litellm/proxy/_experimental/out/model-hub.html b/litellm/proxy/_experimental/out/model-hub/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/model-hub.html
rename to litellm/proxy/_experimental/out/model-hub/index.html
diff --git a/litellm/proxy/_experimental/out/model_hub.html b/litellm/proxy/_experimental/out/model_hub.html
deleted file mode 100644
index fb4d18d07a..0000000000
--- a/litellm/proxy/_experimental/out/model_hub.html
+++ /dev/null
@@ -1 +0,0 @@
-LiteLLM Dashboard
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/model_hub_table.html
rename to litellm/proxy/_experimental/out/model_hub_table/index.html
diff --git a/litellm/proxy/_experimental/out/models-and-endpoints.html b/litellm/proxy/_experimental/out/models-and-endpoints/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/models-and-endpoints.html
rename to litellm/proxy/_experimental/out/models-and-endpoints/index.html
diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html
deleted file mode 100644
index 77ec4ff6f4..0000000000
--- a/litellm/proxy/_experimental/out/onboarding.html
+++ /dev/null
@@ -1 +0,0 @@
-LiteLLM Dashboard
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/organizations.html b/litellm/proxy/_experimental/out/organizations/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/organizations.html
rename to litellm/proxy/_experimental/out/organizations/index.html
diff --git a/litellm/proxy/_experimental/out/playground.html b/litellm/proxy/_experimental/out/playground/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/playground.html
rename to litellm/proxy/_experimental/out/playground/index.html
diff --git a/litellm/proxy/_experimental/out/settings/admin-settings.html b/litellm/proxy/_experimental/out/settings/admin-settings/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/settings/admin-settings.html
rename to litellm/proxy/_experimental/out/settings/admin-settings/index.html
diff --git a/litellm/proxy/_experimental/out/settings/logging-and-alerts.html b/litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/settings/logging-and-alerts.html
rename to litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html
diff --git a/litellm/proxy/_experimental/out/settings/router-settings.html b/litellm/proxy/_experimental/out/settings/router-settings/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/settings/router-settings.html
rename to litellm/proxy/_experimental/out/settings/router-settings/index.html
diff --git a/litellm/proxy/_experimental/out/settings/ui-theme.html b/litellm/proxy/_experimental/out/settings/ui-theme/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/settings/ui-theme.html
rename to litellm/proxy/_experimental/out/settings/ui-theme/index.html
diff --git a/litellm/proxy/_experimental/out/teams.html b/litellm/proxy/_experimental/out/teams/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/teams.html
rename to litellm/proxy/_experimental/out/teams/index.html
diff --git a/litellm/proxy/_experimental/out/test-key.html b/litellm/proxy/_experimental/out/test-key/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/test-key.html
rename to litellm/proxy/_experimental/out/test-key/index.html
diff --git a/litellm/proxy/_experimental/out/tools/mcp-servers.html b/litellm/proxy/_experimental/out/tools/mcp-servers/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/tools/mcp-servers.html
rename to litellm/proxy/_experimental/out/tools/mcp-servers/index.html
diff --git a/litellm/proxy/_experimental/out/tools/vector-stores.html b/litellm/proxy/_experimental/out/tools/vector-stores/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/tools/vector-stores.html
rename to litellm/proxy/_experimental/out/tools/vector-stores/index.html
diff --git a/litellm/proxy/_experimental/out/usage.html b/litellm/proxy/_experimental/out/usage/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/usage.html
rename to litellm/proxy/_experimental/out/usage/index.html
diff --git a/litellm/proxy/_experimental/out/users.html b/litellm/proxy/_experimental/out/users/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/users.html
rename to litellm/proxy/_experimental/out/users/index.html
diff --git a/litellm/proxy/_experimental/out/virtual-keys.html b/litellm/proxy/_experimental/out/virtual-keys/index.html
similarity index 100%
rename from litellm/proxy/_experimental/out/virtual-keys.html
rename to litellm/proxy/_experimental/out/virtual-keys/index.html
diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py
index cbe28849b1..3cbca27ce0 100644
--- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py
+++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py
@@ -1,5 +1,6 @@
#### Analytics Endpoints #####
import os
+
from fastapi import APIRouter
from litellm.types.proxy.discovery_endpoints.ui_discovery_endpoints import (
@@ -14,10 +15,12 @@ router = APIRouter()
"/litellm/.well-known/litellm-ui-config", response_model=UiDiscoveryEndpoints
) # if mounted at root path
async def get_ui_config():
- from litellm.proxy.utils import get_proxy_base_url, get_server_root_path
from litellm.proxy.auth.auth_utils import _has_user_setup_sso
+ from litellm.proxy.utils import get_proxy_base_url, get_server_root_path
- auto_redirect_ui_login_to_sso = os.getenv("AUTO_REDIRECT_UI_LOGIN_TO_SSO", "true").lower() == "true"
+ auto_redirect_ui_login_to_sso = (
+ os.getenv("AUTO_REDIRECT_UI_LOGIN_TO_SSO", "true").lower() == "true"
+ )
admin_ui_disabled = os.getenv("DISABLE_ADMIN_UI", "false").lower() == "true"
return UiDiscoveryEndpoints(
diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
index 7db76fd31d..09c14bc42c 100644
--- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
+++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
@@ -1,8 +1,8 @@
#### CRUD ENDPOINTS for UI Settings #####
import json
-from typing import Any, Dict, List, Union, Optional
+from typing import Any, Dict, List, Optional, Union
-from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
+from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
import litellm
from litellm._logging import verbose_proxy_logger
@@ -10,6 +10,7 @@ from litellm.proxy._types import *
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.proxy.management_endpoints.ui_sso import (
DefaultTeamSSOParams,
+ InProductNudgeResponse,
SSOConfig,
)
@@ -22,11 +23,11 @@ class IPAddress(BaseModel):
class UIThemeConfig(BaseModel):
"""Configuration for UI theme customization"""
-
+
# Logo configuration
logo_url: Optional[str] = Field(
default=None,
- description="URL or path to custom logo image. Can be a local file path or HTTP/HTTPS URL"
+ description="URL or path to custom logo image. Can be a local file path or HTTP/HTTPS URL",
)
@@ -85,7 +86,10 @@ class UISettingsResponse(SettingsResponse):
# Allowlist of UI settings that can be stored
-ALLOWED_UI_SETTINGS_FIELDS = {"disable_model_add_for_internal_users", "disable_team_admin_delete_team_user"}
+ALLOWED_UI_SETTINGS_FIELDS = {
+ "disable_model_add_for_internal_users",
+ "disable_team_admin_delete_team_user",
+}
@router.get(
@@ -434,7 +438,7 @@ async def get_sso_settings():
# Initialize with defaults
sso_settings_dict = {}
-
+
if sso_db_record and sso_db_record.sso_settings:
# Load settings from database
sso_settings_dict = dict(sso_db_record.sso_settings)
@@ -444,26 +448,43 @@ async def get_sso_settings():
role_mappings = None
if role_mappings_data:
from litellm.types.proxy.management_endpoints.ui_sso import RoleMappings
+
if isinstance(role_mappings_data, dict):
role_mappings = RoleMappings(**role_mappings_data)
elif isinstance(role_mappings_data, RoleMappings):
role_mappings = role_mappings_data
-
- decrypted_sso_settings_dict = proxy_config._decrypt_and_set_db_env_variables(environment_variables=sso_settings_dict)
+
+ decrypted_sso_settings_dict = proxy_config._decrypt_and_set_db_env_variables(
+ environment_variables=sso_settings_dict
+ )
# Build SSO config with database values or environment fallback
-
+
sso_config = SSOConfig(
google_client_id=decrypted_sso_settings_dict.get("google_client_id", None),
- google_client_secret=decrypted_sso_settings_dict.get("google_client_secret", None),
- microsoft_client_id=decrypted_sso_settings_dict.get("microsoft_client_id", None),
- microsoft_client_secret=decrypted_sso_settings_dict.get("microsoft_client_secret", None),
+ google_client_secret=decrypted_sso_settings_dict.get(
+ "google_client_secret", None
+ ),
+ microsoft_client_id=decrypted_sso_settings_dict.get(
+ "microsoft_client_id", None
+ ),
+ microsoft_client_secret=decrypted_sso_settings_dict.get(
+ "microsoft_client_secret", None
+ ),
microsoft_tenant=decrypted_sso_settings_dict.get("microsoft_tenant", None),
generic_client_id=decrypted_sso_settings_dict.get("generic_client_id", None),
- generic_client_secret=decrypted_sso_settings_dict.get("generic_client_secret", None),
- generic_authorization_endpoint=decrypted_sso_settings_dict.get("generic_authorization_endpoint", None),
- generic_token_endpoint=decrypted_sso_settings_dict.get("generic_token_endpoint", None),
- generic_userinfo_endpoint=decrypted_sso_settings_dict.get("generic_userinfo_endpoint", None),
+ generic_client_secret=decrypted_sso_settings_dict.get(
+ "generic_client_secret", None
+ ),
+ generic_authorization_endpoint=decrypted_sso_settings_dict.get(
+ "generic_authorization_endpoint", None
+ ),
+ generic_token_endpoint=decrypted_sso_settings_dict.get(
+ "generic_token_endpoint", None
+ ),
+ generic_userinfo_endpoint=decrypted_sso_settings_dict.get(
+ "generic_userinfo_endpoint", None
+ ),
proxy_base_url=decrypted_sso_settings_dict.get("proxy_base_url", None),
user_email=decrypted_sso_settings_dict.get("user_email"),
ui_access_mode=decrypted_sso_settings_dict.get("ui_access_mode"),
@@ -506,10 +527,14 @@ async def update_sso_settings(sso_config: SSOConfig):
"""
Update SSO configuration by saving to the dedicated SSO table.
"""
- import os
import json
+ import os
- from litellm.proxy.proxy_server import prisma_client, store_model_in_db, proxy_config
+ from litellm.proxy.proxy_server import (
+ prisma_client,
+ proxy_config,
+ store_model_in_db,
+ )
if prisma_client is None:
raise HTTPException(
@@ -562,7 +587,9 @@ async def update_sso_settings(sso_config: SSOConfig):
# Clear environment variable if value is null/empty
os.environ.pop(env_var_name, None)
- encrypted_sso_data = proxy_config._encrypt_env_variables(environment_variables=sso_data)
+ encrypted_sso_data = proxy_config._encrypt_env_variables(
+ environment_variables=sso_data
+ )
# Save to dedicated SSO table
await prisma_client.db.litellm_ssoconfig.upsert(
@@ -655,9 +682,10 @@ async def update_ui_theme_settings(theme_config: UIThemeConfig):
Update UI theme configuration.
Updates logo settings for the admin UI.
"""
- from litellm.proxy.proxy_server import proxy_config, store_model_in_db
import os
+ from litellm.proxy.proxy_server import proxy_config, store_model_in_db
+
if store_model_in_db is not True:
raise HTTPException(
status_code=500,
@@ -668,28 +696,30 @@ async def update_ui_theme_settings(theme_config: UIThemeConfig):
# Load existing config
config = await proxy_config.get_config()
-
+
# Update config with UI theme settings
if "general_settings" not in config:
config["general_settings"] = {}
-
+
if "environment_variables" not in config:
config["environment_variables"] = {}
# Convert theme config to dict
theme_data = theme_config.model_dump(exclude_none=True)
-
+
# Store UI theme config in litellm_settings (where it's retrieved from)
if "litellm_settings" not in config:
config["litellm_settings"] = {}
config["litellm_settings"]["ui_theme_config"] = theme_data
-
+
# Update UI_LOGO_PATH environment variable if logo_url is provided
# If logo_url is empty string, None, or null, remove the environment variable to use default
logo_url = theme_data.get("logo_url")
verbose_proxy_logger.debug(f"Updating logo_url: {logo_url}")
-
- if logo_url and isinstance(logo_url, str) and logo_url.strip(): # Check if logo_url exists and is not empty/whitespace
+
+ if (
+ logo_url and isinstance(logo_url, str) and logo_url.strip()
+ ): # Check if logo_url exists and is not empty/whitespace
config["environment_variables"]["UI_LOGO_PATH"] = logo_url
os.environ["UI_LOGO_PATH"] = logo_url
verbose_proxy_logger.debug(f"Set UI_LOGO_PATH to: {logo_url}")
@@ -704,12 +734,15 @@ async def update_ui_theme_settings(theme_config: UIThemeConfig):
# Handle environment variable encryption if needed
stored_config = config.copy()
- if "environment_variables" in stored_config and len(stored_config["environment_variables"]) > 0:
+ if (
+ "environment_variables" in stored_config
+ and len(stored_config["environment_variables"]) > 0
+ ):
# Only encrypt if there are environment variables to encrypt
stored_config["environment_variables"] = proxy_config._encrypt_env_variables(
environment_variables=stored_config["environment_variables"]
)
-
+
# Save the updated config
await proxy_config.save_config(new_config=stored_config)
@@ -720,6 +753,34 @@ async def update_ui_theme_settings(theme_config: UIThemeConfig):
}
+@router.get(
+ "/in_product_nudges",
+ tags=["UI Settings"],
+ dependencies=[Depends(user_api_key_auth)],
+ response_model=InProductNudgeResponse,
+)
+async def get_in_product_nudges():
+ """
+ Get in-product nudges configuration.
+ """
+ from litellm.proxy.proxy_server import prisma_client
+
+ if prisma_client is None:
+ raise HTTPException(
+ status_code=500,
+ detail={"error": "Database not connected. Please connect a database."},
+ )
+
+ db_record = await prisma_client.db.litellm_dailytagspend.find_first(
+ where={"tag": "User-Agent: claude-cli"}
+ )
+
+ if db_record:
+ return InProductNudgeResponse(is_claude_code_enabled=True)
+
+ return InProductNudgeResponse(is_claude_code_enabled=False)
+
+
@router.get(
"/get/ui_settings",
tags=["UI Settings"],
@@ -752,7 +813,9 @@ async def get_ui_settings():
ui_settings = dict(ui_settings_json)
# Sanitize any unexpected keys from persisted config before returning
- ui_settings = {k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS}
+ ui_settings = {
+ k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS
+ }
# Build config-like object for schema helper
config: Dict[str, Any] = {"litellm_settings": {"ui_settings": ui_settings}}
@@ -823,6 +886,7 @@ async def update_ui_settings(
"settings": ui_settings,
}
+
@router.post(
"/upload/logo",
tags=["UI Theme Settings"],
@@ -839,35 +903,35 @@ async def upload_logo(file: UploadFile = File(...)):
# Validate file type
allowed_extensions = {".png", ".jpg", ".jpeg", ".svg"}
file_extension = Path(file.filename or "").suffix.lower()
-
+
if file_extension not in allowed_extensions:
raise HTTPException(
status_code=400,
- detail=f"Invalid file type. Allowed types: {', '.join(allowed_extensions)}"
+ detail=f"Invalid file type. Allowed types: {', '.join(allowed_extensions)}",
)
-
+
# Validate file size (max 5MB)
file_content = await file.read()
if len(file_content) > 5 * 1024 * 1024: # 5MB
raise HTTPException(
- status_code=400,
- detail="File size too large. Maximum size is 5MB."
+ status_code=400, detail="File size too large. Maximum size is 5MB."
)
-
+
# Create uploads directory if it doesn't exist
current_dir = os.path.dirname(os.path.abspath(__file__))
upload_dir = os.path.join(current_dir, "..", "uploads")
os.makedirs(upload_dir, exist_ok=True)
-
+
# Generate unique filename
from litellm._uuid import uuid
+
unique_filename = f"logo_{uuid.uuid4().hex}{file_extension}"
file_path = os.path.join(upload_dir, unique_filename)
-
+
# Save the file
with open(file_path, "wb") as buffer:
buffer.write(file_content)
-
+
return {
"message": "Logo uploaded successfully",
"status": "success",
diff --git a/litellm/types/proxy/management_endpoints/ui_sso.py b/litellm/types/proxy/management_endpoints/ui_sso.py
index 187d8c97c0..c9d998f6a9 100644
--- a/litellm/types/proxy/management_endpoints/ui_sso.py
+++ b/litellm/types/proxy/management_endpoints/ui_sso.py
@@ -1,11 +1,10 @@
from typing import Dict, List, Literal, Optional, Union
-from pydantic import Field
+from pydantic import BaseModel, Field
from typing_extensions import TypedDict
-from litellm.types.utils import LiteLLMPydanticObjectBase
-
from litellm.proxy._types import LitellmUserRoles
+from litellm.types.utils import LiteLLMPydanticObjectBase
class LiteLLM_UpperboundKeyGenerateParams(LiteLLMPydanticObjectBase):
@@ -28,6 +27,7 @@ class LiteLLM_UpperboundKeyGenerateParams(LiteLLMPydanticObjectBase):
tpm_limit: Optional[int] = None
rpm_limit: Optional[int] = None
+
class MicrosoftGraphAPIUserGroupDirectoryObject(TypedDict, total=False):
"""Model for Microsoft Graph API directory object"""
@@ -65,7 +65,7 @@ class AccessControl_UI_AccessMode(LiteLLMPydanticObjectBase):
class RoleMappings(LiteLLMPydanticObjectBase):
"""
Configuration for mapping SSO groups to LiteLLM roles.
-
+
The system will look at the group_claim field in the SSO token to determine
which role to assign the user based on the roles mapping.
"""
@@ -78,11 +78,11 @@ class RoleMappings(LiteLLMPydanticObjectBase):
)
default_role: Optional[LitellmUserRoles] = Field(
default=None,
- description="Default role to assign if user's groups don't match any role mappings. Must be a valid LitellmUserRoles value (e.g., 'proxy_admin', 'internal_user', 'proxy_admin_viewer')"
+ description="Default role to assign if user's groups don't match any role mappings. Must be a valid LitellmUserRoles value (e.g., 'proxy_admin', 'internal_user', 'proxy_admin_viewer')",
)
roles: Dict[LitellmUserRoles, List[str]] = Field(
default_factory=dict,
- description="Mapping of LiteLLM role names to arrays of SSO group names. Example: {'proxy_admin': ['group-1', 'group-2'], 'proxy_admin_viewer': ['group-3']}"
+ description="Mapping of LiteLLM role names to arrays of SSO group names. Example: {'proxy_admin': ['group-1', 'group-2'], 'proxy_admin_viewer': ['group-3']}",
)
@@ -185,3 +185,10 @@ class DefaultTeamSSOParams(LiteLLMPydanticObjectBase):
default=None,
description="Default rpm limit for new automatically created teams",
)
+
+
+class InProductNudgeResponse(BaseModel):
+ is_claude_code_enabled: bool = Field(
+ default=False,
+ description="Whether the Claude Code nudge should be shown.",
+ )
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx
index ac019a7a8c..8a56287e15 100644
--- a/ui/litellm-dashboard/src/app/page.tsx
+++ b/ui/litellm-dashboard/src/app/page.tsx
@@ -17,7 +17,7 @@ import { Team } from "@/components/key_team_helpers/key_list";
import { MCPServers } from "@/components/mcp_tools";
import ModelHubTable from "@/components/AIHub/ModelHubTable";
import Navbar from "@/components/navbar";
-import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
+import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName, getInProductNudgesCall } from "@/components/networking";
import NewUsagePage from "@/components/UsagePage/components/UsagePageView";
import OldTeams from "@/components/OldTeams";
import { fetchUserModels } from "@/components/organisms/create_key_button";
@@ -27,7 +27,7 @@ import PromptsPanel from "@/components/prompts";
import PublicModelHub from "@/components/public_model_hub";
import { SearchTools } from "@/components/search_tools";
import Settings from "@/components/settings";
-import { SurveyPrompt, SurveyModal } from "@/components/survey";
+import { SurveyPrompt, SurveyModal, ClaudeCodePrompt, ClaudeCodeModal } from "@/components/survey";
import TagManagement from "@/components/tag_management";
import TransformRequestPanel from "@/components/transform_request";
import UIThemeSettings from "@/components/ui_theme_settings";
@@ -124,6 +124,11 @@ export default function CreateKeyPage() {
const [showSurveyPrompt, setShowSurveyPrompt] = useState(true);
const [showSurveyModal, setShowSurveyModal] = useState(false);
+ // Claude Code feedback state
+ const [isClaudeCode, setIsClaudeCode] = useState(false);
+ const [showClaudeCodePrompt, setShowClaudeCodePrompt] = useState(false);
+ const [showClaudeCodeModal, setShowClaudeCodeModal] = useState(false);
+
const invitation_id = searchParams.get("invitation_id");
// Get page from URL, default to 'api-keys' if not present
@@ -267,6 +272,29 @@ export default function CreateKeyPage() {
}
}, [accessToken, userID, userRole]);
+ // Fetch in-product nudges configuration from backend
+ useEffect(() => {
+ if (accessToken && token) {
+ (async () => {
+ try {
+ const nudgesConfig = await getInProductNudgesCall(accessToken);
+ const isUsingClaudeCode = nudgesConfig?.is_claude_code_enabled || false;
+ setIsClaudeCode(isUsingClaudeCode);
+
+ // Show Claude Code prompt on login if enabled
+ if (isUsingClaudeCode) {
+ setShowClaudeCodePrompt(true);
+ // Don't show the regular survey prompt if showing Claude Code prompt
+ setShowSurveyPrompt(false);
+ }
+ } catch (error) {
+ console.error("Failed to fetch in-product nudges:", error);
+ // Silently fail and don't show Claude Code nudge
+ }
+ })();
+ }
+ }, [accessToken, token]);
+
// Auto-dismiss survey prompt after 15 seconds
useEffect(() => {
if (showSurveyPrompt && !showSurveyModal) {
@@ -277,6 +305,16 @@ export default function CreateKeyPage() {
}
}, [showSurveyPrompt, showSurveyModal]);
+ // Auto-dismiss Claude Code prompt after 15 seconds
+ useEffect(() => {
+ if (showClaudeCodePrompt && !showClaudeCodeModal) {
+ const timer = setTimeout(() => {
+ setShowClaudeCodePrompt(false);
+ }, 15000);
+ return () => clearTimeout(timer);
+ }
+ }, [showClaudeCodePrompt, showClaudeCodeModal]);
+
const handleOpenSurvey = () => {
setShowSurveyPrompt(false);
setShowSurveyModal(true);
@@ -296,6 +334,25 @@ export default function CreateKeyPage() {
setShowSurveyPrompt(true);
};
+ const handleOpenClaudeCode = () => {
+ setShowClaudeCodePrompt(false);
+ setShowClaudeCodeModal(true);
+ };
+
+ const handleDismissClaudeCodePrompt = () => {
+ setShowClaudeCodePrompt(false);
+ };
+
+ const handleClaudeCodeComplete = () => {
+ setShowClaudeCodeModal(false);
+ };
+
+ const handleClaudeCodeModalClose = () => {
+ // If they close the modal without completing, show the prompt again
+ setShowClaudeCodeModal(false);
+ setShowClaudeCodePrompt(true);
+ };
+
if (authLoading || redirectToLogin) {
return ;
}
@@ -503,6 +560,18 @@ export default function CreateKeyPage() {
onClose={handleSurveyModalClose}
onComplete={handleSurveyComplete}
/>
+
+ {/* Claude Code Components */}
+
+
)}
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 9079fd6a7e..5bd756f1d1 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -7,6 +7,7 @@ import {
BarChartOutlined,
BgColorsOutlined,
BlockOutlined,
+ BookOutlined,
CreditCardOutlined,
DatabaseOutlined,
ExperimentOutlined,
@@ -47,6 +48,7 @@ interface MenuItem {
roles?: string[];
children?: MenuItem[];
icon?: React.ReactNode;
+ external_url?: string;
}
// Group configuration
@@ -213,6 +215,13 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse
label: "AI Hub",
icon: ,
},
+ {
+ key: "learning-resources",
+ page: "learning-resources",
+ label: "Learning Resources",
+ icon: ,
+ external_url: "https://models.litellm.ai/cookbook",
+ },
{
key: "experimental",
page: "experimental",
@@ -252,7 +261,7 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse
page: "usage",
label: "Old Usage",
icon: ,
- },
+ }
],
},
],
@@ -364,9 +373,23 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse
key: child.key,
icon: child.icon,
label: child.label,
- onClick: () => navigateToPage(child.page),
+ onClick: () => {
+ if (child.external_url) {
+ window.open(child.external_url, "_blank");
+ } else {
+ navigateToPage(child.page);
+ }
+ },
})),
- onClick: !item.children ? () => navigateToPage(item.page) : undefined,
+ onClick: !item.children
+ ? () => {
+ if (item.external_url) {
+ window.open(item.external_url, "_blank");
+ } else {
+ navigateToPage(item.page);
+ }
+ }
+ : undefined,
})),
});
});
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 82894dfb0e..c975877d06 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -35,6 +35,36 @@ export const getCallbackConfigsCall = async (accessToken: string) => {
throw error;
}
};
+
+export const getInProductNudgesCall = async (accessToken: string) => {
+ /**
+ * Get in-product nudges configuration.
+ */
+ try {
+ let url = proxyBaseUrl ? `${proxyBaseUrl}/in_product_nudges` : `/in_product_nudges`;
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ [globalLitellmHeaderName]: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ const errorMessage = deriveErrorMessage(errorData);
+ handleError(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error("Failed to get in-product nudges:", error);
+ throw error;
+ }
+};
/**
* Helper file for calls being made to proxy
*/
diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx
new file mode 100644
index 0000000000..94527c160c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import { X, Code, ExternalLink } from "lucide-react";
+import { Button } from "antd";
+
+interface ClaudeCodeModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onComplete: () => void;
+}
+
+const GOOGLE_FORM_URL = "https://forms.gle/LZeJQ3XytBakckYa9";
+
+export function ClaudeCodeModal({ isOpen, onClose, onComplete }: ClaudeCodeModalProps) {
+ if (!isOpen) return null;
+
+ const handleOpenForm = () => {
+ window.open(GOOGLE_FORM_URL, "_blank", "noopener,noreferrer");
+ onComplete();
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+
+
+
+ Claude Code Feedback
+
+
+
+
+ {/* Content */}
+
+
+ Help us improve your experience
+
+
+ We'd love to hear about your experience using LiteLLM with Claude Code. Your feedback helps us improve the product for everyone.
+
+
+ This brief survey takes about 2-3 minutes to complete.
+
+
+
}
+ style={{ backgroundColor: '#7c3aed', borderColor: '#7c3aed' }}
+ >
+ Open Feedback Form
+
+
+
+
+ );
+}
+
diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx
new file mode 100644
index 0000000000..9006575ce0
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { Code } from "lucide-react";
+import { NudgePrompt } from "./NudgePrompt";
+
+interface ClaudeCodePromptProps {
+ onOpen: () => void;
+ onDismiss: () => void;
+ isVisible: boolean;
+}
+
+export function ClaudeCodePrompt({ onOpen, onDismiss, isVisible }: ClaudeCodePromptProps) {
+ return (
+
+ );
+}
+
diff --git a/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx b/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx
new file mode 100644
index 0000000000..9095c6c21c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx
@@ -0,0 +1,91 @@
+import React, { useEffect, useState } from "react";
+import { X, LucideIcon } from "lucide-react";
+import { Button } from "antd";
+
+interface NudgePromptProps {
+ onOpen: () => void;
+ onDismiss: () => void;
+ isVisible: boolean;
+ title: string;
+ description: string;
+ buttonText: string;
+ icon: LucideIcon;
+ accentColor: string;
+ buttonStyle?: React.CSSProperties;
+}
+
+const DISMISS_DURATION = 15000; // 15 seconds
+
+export function NudgePrompt({
+ onOpen,
+ onDismiss,
+ isVisible,
+ title,
+ description,
+ buttonText,
+ icon: Icon,
+ accentColor,
+ buttonStyle,
+}: NudgePromptProps) {
+ const [progress, setProgress] = useState(100);
+
+ useEffect(() => {
+ if (!isVisible) {
+ setProgress(100);
+ return;
+ }
+
+ const startTime = Date.now();
+ const interval = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+ const remaining = Math.max(0, 100 - (elapsed / DISMISS_DURATION) * 100);
+ setProgress(remaining);
+
+ if (remaining <= 0) {
+ clearInterval(interval);
+ }
+ }, 50);
+
+ return () => clearInterval(interval);
+ }, [isVisible]);
+
+ if (!isVisible) return null;
+
+ return (
+
+ {/* Progress bar at top showing time remaining */}
+
+
+
+
+
+
{description}
+
+
+
+
+ );
+}
+
diff --git a/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx
index 69a886bdde..a41acb265a 100644
--- a/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx
+++ b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx
@@ -1,6 +1,6 @@
-import React, { useEffect, useState } from "react";
-import { MessageSquare, X } from "lucide-react";
-import { Button } from "antd";
+import React from "react";
+import { MessageSquare } from "lucide-react";
+import { NudgePrompt } from "./NudgePrompt";
interface SurveyPromptProps {
onOpen: () => void;
@@ -8,70 +8,18 @@ interface SurveyPromptProps {
isVisible: boolean;
}
-const DISMISS_DURATION = 15000; // 15 seconds
-
export function SurveyPrompt({ onOpen, onDismiss, isVisible }: SurveyPromptProps) {
- const [progress, setProgress] = useState(100);
-
- useEffect(() => {
- if (!isVisible) {
- setProgress(100);
- return;
- }
-
- const startTime = Date.now();
- const interval = setInterval(() => {
- const elapsed = Date.now() - startTime;
- const remaining = Math.max(0, 100 - (elapsed / DISMISS_DURATION) * 100);
- setProgress(remaining);
-
- if (remaining <= 0) {
- clearInterval(interval);
- }
- }, 50);
-
- return () => clearInterval(interval);
- }, [isVisible]);
-
- if (!isVisible) return null;
-
return (
-
- {/* Progress bar at top showing time remaining */}
-
-
-
-
-
-
- Quick feedback
-
-
-
-
-
- Help us improve LiteLLM! Share your experience in 5 quick questions.
-
-
-
-
-
+
);
}
diff --git a/ui/litellm-dashboard/src/components/survey/index.tsx b/ui/litellm-dashboard/src/components/survey/index.tsx
index 7c36419980..fde05084b7 100644
--- a/ui/litellm-dashboard/src/components/survey/index.tsx
+++ b/ui/litellm-dashboard/src/components/survey/index.tsx
@@ -1,3 +1,6 @@
export { SurveyPrompt } from "./SurveyPrompt";
export { SurveyModal } from "./SurveyModal";
+export { ClaudeCodePrompt } from "./ClaudeCodePrompt";
+export { ClaudeCodeModal } from "./ClaudeCodeModal";
+export { NudgePrompt } from "./NudgePrompt";