From 4fa1470fef7c4f18ae575d69bb7368a4faed0cfb Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Sun, 18 Jan 2026 09:35:20 +0530 Subject: [PATCH] Add in-product nudge for claude code feedback survey + new learning centre (#19303) * fix(proxy_setting_endpoints.py): add new GET /in_product_nudges route allows for context-based nudges * feat: initial commit, adding in-product nudge for claude code usage helps us talk to more litellm x claude code users * fix: link out to google form for claude code in-product nudge * feat(index.json): add new guide * feat(index.json): add new claude code guides * fix: link out * fix: remove baselines --- cookbook/ai_coding_tool_guides/index.json | 36 +++++ .../migration.sql | 2 - .../migration.sql | 2 - litellm/proxy/_experimental/out/404.html | 1 - .../index.html} | 0 .../index.html} | 0 .../{budgets.html => budgets/index.html} | 0 .../{caching.html => caching/index.html} | 0 .../{old-usage.html => old-usage/index.html} | 0 .../{prompts.html => prompts/index.html} | 0 .../index.html} | 0 .../proxy/_experimental/out/guardrails.html | 1 - .../out/{login.html => login/index.html} | 0 .../out/{logs.html => logs/index.html} | 0 .../{callback.html => callback/index.html} | 0 .../{model-hub.html => model-hub/index.html} | 0 .../proxy/_experimental/out/model_hub.html | 1 - .../index.html} | 0 .../index.html} | 0 .../proxy/_experimental/out/onboarding.html | 1 - .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{ui-theme.html => ui-theme/index.html} | 0 .../out/{teams.html => teams/index.html} | 0 .../{test-key.html => test-key/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../out/{usage.html => usage/index.html} | 0 .../out/{users.html => users/index.html} | 0 .../index.html} | 0 .../ui_discovery_endpoints.py | 7 +- .../proxy_setting_endpoints.py | 140 +++++++++++++----- .../proxy/management_endpoints/ui_sso.py | 19 ++- ui/litellm-dashboard/src/app/page.tsx | 73 ++++++++- .../src/components/leftnav.tsx | 29 +++- .../src/components/networking.tsx | 30 ++++ .../src/components/survey/ClaudeCodeModal.tsx | 69 +++++++++ .../components/survey/ClaudeCodePrompt.tsx | 26 ++++ .../src/components/survey/NudgePrompt.tsx | 91 ++++++++++++ .../src/components/survey/SurveyPrompt.tsx | 78 ++-------- .../src/components/survey/index.tsx | 3 + 44 files changed, 485 insertions(+), 124 deletions(-) delete mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120021_baseline_diff/migration.sql delete mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20251115120539_baseline_diff/migration.sql delete mode 100644 litellm/proxy/_experimental/out/404.html rename litellm/proxy/_experimental/out/{api-reference.html => api-reference/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{api-playground.html => api-playground/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{budgets.html => budgets/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{caching.html => caching/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{old-usage.html => old-usage/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{prompts.html => prompts/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{tag-management.html => tag-management/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/guardrails.html rename litellm/proxy/_experimental/out/{login.html => login/index.html} (100%) rename litellm/proxy/_experimental/out/{logs.html => logs/index.html} (100%) rename litellm/proxy/_experimental/out/mcp/oauth/{callback.html => callback/index.html} (100%) rename litellm/proxy/_experimental/out/{model-hub.html => model-hub/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/model_hub.html rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) rename litellm/proxy/_experimental/out/{models-and-endpoints.html => models-and-endpoints/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/onboarding.html rename litellm/proxy/_experimental/out/{organizations.html => organizations/index.html} (100%) rename litellm/proxy/_experimental/out/{playground.html => playground/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{admin-settings.html => admin-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{logging-and-alerts.html => logging-and-alerts/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{router-settings.html => router-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{ui-theme.html => ui-theme/index.html} (100%) rename litellm/proxy/_experimental/out/{teams.html => teams/index.html} (100%) rename litellm/proxy/_experimental/out/{test-key.html => test-key/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{mcp-servers.html => mcp-servers/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{vector-stores.html => vector-stores/index.html} (100%) rename litellm/proxy/_experimental/out/{usage.html => usage/index.html} (100%) rename litellm/proxy/_experimental/out/{users.html => users/index.html} (100%) rename litellm/proxy/_experimental/out/{virtual-keys.html => virtual-keys/index.html} (100%) create mode 100644 ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx create mode 100644 ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx create mode 100644 ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx 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 Dashboard

404

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

+ + +
+
+
+ ); +} + 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 */} +
+
+
+ +
+
+
+ + {title} +
+ +
+ +

{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";