import asyncio import copy import enum import importlib import inspect import io import os import random import re import secrets import shutil import socket import subprocess import sys import threading import time import traceback import warnings from collections.abc import ( AsyncGenerator, AsyncIterator, Awaitable, Callable, Collection, Mapping, MutableMapping, Sequence, ) from datetime import datetime, timedelta, timezone from itertools import chain from types import MappingProxyType, UnionType from typing import ( TYPE_CHECKING, Any, Final, Literal, NamedTuple, Optional, Protocol, TypeAlias, TypedDict, Union, cast, get_args, get_origin, get_type_hints, ) import anyio import websockets import websockets.exceptions from pydantic import BaseModel, Json, JsonValue, TypeAdapter, ValidationError from pydantic.fields import FieldInfo, PydanticUndefined from typing_extensions import NotRequired, ReadOnly, assert_never from litellm._uuid import uuid from litellm.constants import ( AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_CONNECTOR_LIMIT_PER_HOST, AIOHTTP_KEEPALIVE_TIMEOUT, AIOHTTP_NEEDS_CLEANUP_CLOSED, AIOHTTP_TTL_DNS_CACHE, AUDIO_SPEECH_CHUNK_SIZE, BACKGROUND_HEALTH_CHECK_DB_SAVE_JOB_NAME, BASE_MCP_ROUTE, DAILY_TAG_SPEND_BATCH_MULTIPLIER, DEFAULT_MAX_RECURSE_DEPTH, DEFAULT_SHARED_HEALTH_CHECK_LOCK_TTL, DEFAULT_SHARED_HEALTH_CHECK_TTL, DEFAULT_SLACK_ALERTING_THRESHOLD, LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS, LITELLM_SETTINGS_SAFE_DB_OVERRIDES, LITELLM_UI_ALLOW_HEADERS, LITELLM_UI_SESSION_DURATION, RUNTIME_UPDATABLE_ROUTER_SETTINGS, ) from litellm.litellm_core_utils.asyncify import asyncify from litellm.litellm_core_utils.bug_report import ( allowlisted, bug_report_notice, should_report_bug, ) from litellm.litellm_core_utils.litellm_logging import ( _init_custom_logger_compatible_class, ) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_loads import safe_json_loads from litellm.litellm_core_utils.token_counter import offload_token_count from litellm.proxy._types import ( UI_TEAM_ID, CallbackDelete, CallInfo, CommonProxyErrors, ConfigFieldDelete, ConfigFieldInfo, ConfigFieldUpdate, ConfigGeneralSettings, ConfigList, ConfigYAML, CoordinationRedisParams, EnterpriseLicenseData, FieldDetail, InvitationClaim, InvitationDelete, InvitationModel, InvitationNew, InvitationUpdate, LiteLLM_EndUserTable, Litellm_EntityType, LiteLLM_JWTAuth, LiteLLM_TagTable, LiteLLM_TeamTable, LiteLLM_TeamTableCachedObj, LiteLLM_UserTable, LitellmUserRoles, ModelAccessDeniedProxyException, PassThroughGenericEndpoint, ProxyErrorTypes, ProxyException, SpecialModelNames, SupportedDBObjectType, TeamDefaultSettings, TokenCountRequest, TransformRequestBody, UserAPIKeyAuth, ) from litellm.proxy.common_utils.cache_pydantic_utils import CacheCodec from litellm.proxy.common_utils.callback_utils import ( is_sensitive_callback_key, normalize_callback_names, process_callback, strip_callback_config, ) from litellm.proxy.common_utils.realtime_utils import _realtime_request_body from litellm.router_utils.add_retry_fallback_headers import ( get_fallback_errors_from_headers, get_hidden_params_dict, ) from litellm.router_utils.auto_router_model_naming import ( GATED_AUTO_ROUTER_CAPABILITIES, STRATEGY_ROUTER_PARAM_FIELDS, capability_limit_violation, carries_complexity_router_settings, count_capability_routers, validate_complexity_router_config_placement, ) from litellm.router_utils.auto_router_tuning_baseline import ( TUNING_BASELINE_PARAM_NAME, mutable_tuned_identities, snapshot_tuning_baselines, tuning_limit_violation, ) from litellm.router_utils.routing_groups import parse_routing_groups from litellm.types.caching import RedisPipelineIncrementOperation from litellm.types.utils import ( PRICING_OVERRIDES_KEY, ModelResponse, ModelResponseStream, StreamingChoices, TextCompletionResponse, TokenCountResponse, echoed_cost_map_pricing_fields, is_server_derived_pricing_key, pricing_override_fields, ) from litellm.utils import cost_map_omits_token_price, load_credentials_from_list if TYPE_CHECKING: from aiohttp import ClientSession from fastapi.routing import APIRoute from opentelemetry.trace import Span as _Span from prisma import models as prisma_models from litellm.integrations.opentelemetry import OpenTelemetry from litellm.proxy.health_check_utils.shared_health_check_manager import SharedHealthCheckManager Span = _Span | Any else: Span = Any OpenTelemetry = Any REALTIME_REQUEST_SCOPE_TEMPLATE: Final[dict[str, object]] = { "type": "http", "method": "POST", "path": "/v1/realtime", } def showwarning(message, category, filename, lineno, file=None, line=None): traceback_info: Final = f"{filename}:{lineno}: {category.__name__}: {message}\n" if file is not None: file.write(traceback_info) warnings.showwarning = showwarning warnings.filterwarnings("default", category=UserWarning) # Your client code here messages: Final[list] = [] sys.path.insert(0, os.path.abspath("../..")) # Adds the parent directory to the system path - for litellm local dev try: import logging import backoff import fastapi import orjson import yaml from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger except ImportError as e: raise ImportError(f"Missing dependency {e}. Run `pip install 'litellm[proxy]'`") list_of_messages: Final = [ "'The thing I wish you improved is...'", "'A feature I really want is...'", "'The worst thing about this product is...'", "'This product would be better if...'", "'I don't like how this works...'", "'It would help me if you could add...'", "'This feature doesn't meet my needs because...'", "'I get frustrated when the product...'", ] def generate_feedback_box(): box_width: Final = 60 # Select a random message message: Final = random.choice(list_of_messages) print() # noqa: T201 print("\033[1;37m" + "#" + "-" * box_width + "#\033[0m") # noqa: T201 print("\033[1;37m" + "#" + " " * box_width + "#\033[0m") # noqa: T201 print("\033[1;37m" + f"# {message:^59} #\033[0m") # noqa: T201 print( # noqa: T201 "\033[1;37m" + "# {:^59} #\033[0m".format("https://github.com/BerriAI/litellm/issues/new") ) print("\033[1;37m" + "#" + " " * box_width + "#\033[0m") # noqa: T201 print("\033[1;37m" + "#" + "-" * box_width + "#\033[0m") # noqa: T201 print() # noqa: T201 print(" Thank you for using LiteLLM! - Krrish & Ishaan") # noqa: T201 print() # noqa: T201 print() # noqa: T201 print() # noqa: T201 print( # noqa: T201 "\033[1;31mGive Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new\033[0m" ) print() # noqa: T201 print() # noqa: T201 import contextlib from collections import defaultdict from contextlib import asynccontextmanager from functools import lru_cache, partial import litellm import litellm._redis from litellm import Router from litellm._logging import _redact_string, verbose_proxy_logger, verbose_router_logger from litellm.caching.caching import DualCache, RedisCache from litellm.caching.redis_cache import RedisCircuitBreakerOpenError, is_redis_timeout_failure from litellm.caching.redis_cluster_cache import RedisClusterCache from litellm.constants import ( _REALTIME_BODY_CACHE_SIZE, APSCHEDULER_COALESCE, APSCHEDULER_MAX_INSTANCES, APSCHEDULER_MISFIRE_GRACE_TIME, APSCHEDULER_REPLACE_EXISTING, CLI_SSO_SESSION_TTL_SECONDS, DAILY_GLOBAL_SPEND_RECONCILE_JOB_ID, DAYS_IN_A_MONTH, DEFAULT_HEALTH_CHECK_INTERVAL, DEFAULT_MODEL_CREATED_AT_TIME, GLOBAL_PROXY_SPEND_CACHE_KEY, LITELLM_PROXY_ADMIN_NAME, LITELLM_PROXY_BUDGET_NAME, MONTHLY_SPEND_REPORT_JOB_ID, PROMETHEUS_FALLBACK_STATS_JOB_ID, PROMETHEUS_FALLBACK_STATS_SEND_TIME_HOURS, PROXY_BATCH_POLLING_ENABLED, PROXY_BATCH_POLLING_INTERVAL, PROXY_BATCH_WRITE_AT, PROXY_BUDGET_RESCHEDULER_MAX_TIME, PROXY_BUDGET_RESCHEDULER_MIN_TIME, PROXY_CONFIG_RELOAD_INTERVAL_SECONDS, REALTIME_SESSION_FAILURE_LOGGED_KEY, REALTIME_SESSION_SUCCESS_LOGGED_KEY, ROUTER_SETTINGS_MANAGED_OUTSIDE_CONFIG, USER_SPEND_ALERTS_JOB_ID, WEEKLY_SPEND_REPORT_JOB_ID, ) from litellm.exceptions import RejectedRequestError from litellm.integrations.custom_guardrail import CustomGuardrail, ModifyResponseException from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting from litellm.litellm_core_utils.agentic_loop_settings import ( validated_max_agentic_loops, ) from litellm.litellm_core_utils.audio_utils.utils import resolve_speech_media_type from litellm.litellm_core_utils.core_helpers import ( _get_parent_otel_span_from_kwargs, drop_params_flag, get_litellm_metadata_from_kwargs, ) from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.realtime_errors import ( realtime_error_event, websocket_close_reason, ) from litellm.litellm_core_utils.sensitive_data_masker import ( SensitiveDataMasker, mask_sensitive_keys, ) from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.openai_like.model_info import MODEL_INFO_REFRESH_SECONDS from litellm.llms.vertex_ai.vertex_llm_base import VertexBase from litellm.proxy._experimental.mcp_server.byok_credential_cache import byok_credential_cache from litellm.proxy._lazy_features import attach_lazy_features, reserve_lazy_slot from litellm.proxy._types import * from litellm.proxy.analytics_endpoints.analytics_endpoints import ( router as analytics_router, ) from litellm.proxy.auth.auth_checks import ( ROLE_BASED_PERMISSIONS_ADAPTER, ExperimentalUIJWTToken, can_key_call_resolved_model, get_team_object, log_db_metrics, ) from litellm.proxy.auth.auth_utils import ( check_response_size_is_safe, is_request_body_safe, log_once_if_budget_reservation_disabled, warn_once_if_custom_auth_skips_common_checks, ) from litellm.proxy.auth.fallback_budget import router_fallback_budget_check from litellm.proxy.auth.fallback_model_access import router_fallback_access_check from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.auth.litellm_license import AUTO_ROUTER_LICENSE_REMEDY, LicenseCheck from litellm.proxy.auth.login_throttle import ( LoginThrottle, declared_proxy_ranges, warn_login_counters_are_per_worker, warn_source_login_limit_is_off, ) from litellm.proxy.auth.master_key_boot_check import ( MASTER_KEY_ENV_VAR, SALT_KEY_ENV_VAR, WEAK_OR_UNSET_MASTER_KEY_OVERRIDE_ENV_VAR, announce_on_stderr_at_exit, enforce_master_key_boot_verdict, master_key_boot_verdict, with_stored_secrets_counted, ) from litellm.proxy.auth.model_checks import ( expand_wildcard_deployments_for_model_info, get_all_fallbacks, get_complete_model_list, get_key_models, get_mcp_server_ids, get_team_models, ) from litellm.proxy.auth.password_policy import validate_password_not_breached, validate_password_policy from litellm.proxy.auth.user_api_key_auth import ( _fetch_global_spend_with_event_coordination, user_api_key_auth, user_api_key_auth_websocket, ) from litellm.proxy.batches_endpoints.endpoints import router as batches_router from litellm.proxy.bug_report_config import build_proxy_bug_report ## Import All Misc routes here ## from litellm.proxy.caching_routes import router as caching_router from litellm.proxy.common_request_processing import ( KNOWN_PROXY_ROUTES, ProxyBaseLLMRequestProcessing, _is_azure_model_router_request, _should_return_raw_model_name, create_response, log_llm_api_exception, open_sse_before_first_byte, request_litellm_call_id, resolve_litellm_call_id, ttft_keepalive_interval, ) from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import ( AuthCacheInvalidationSubscriber, ) from litellm.proxy.common_utils.callback_utils import initialize_callbacks_on_proxy from litellm.proxy.common_utils.config_includes import resolve_include_file_path, resolve_includes from litellm.proxy.common_utils.config_sync_pubsub import ConfigSyncSubscriber from litellm.proxy.common_utils.debug_utils import init_verbose_loggers from litellm.proxy.common_utils.debug_utils import router as debugging_endpoints_router from litellm.proxy.common_utils.encrypt_decrypt_utils import ( decrypt_value_helper, encrypt_value_helper, ) from litellm.proxy.common_utils.error_body_call_id import JSON_OBJECT, error_body_call_id, with_call_id from litellm.proxy.common_utils.healthy_model_filter import ( get_hidden_unhealthy_model_names, is_healthy_only_listing_default, ) from litellm.proxy.common_utils.html_forms.default_credentials_hint import should_hide_default_credentials_hint from litellm.proxy.common_utils.html_forms.ui_login import build_ui_login_form from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_get_request_headers, check_file_size_under_limit, get_form_data, ) from litellm.proxy.common_utils.load_config_utils import get_config_from_bucket from litellm.proxy.common_utils.model_deprecation import collect_model_deprecations from litellm.proxy.common_utils.model_listing_utils import ( ClaudeCodeRoutingNames, TeamModelNameTranslator, claude_code_view_ids, configured_display_names, is_claude_code_client, ) from litellm.proxy.common_utils.openai_endpoint_utils import ( remove_sensitive_info_from_deployment, ) from litellm.proxy.common_utils.openai_error_payload import ( LITELLM_CALL_ID_HEADER, headers_with_litellm_call_id, litellm_call_id_headers, with_litellm_call_id, ) from litellm.proxy.common_utils.periodic_reload_schedule import ( MODEL_COST_MAP_RELOAD_PARAM_NAME, clear_reload_interval, pod_reload_is_due, read_reload_schedule, record_manual_reload, record_reload_run, reload_schedule_status, utc_now, write_reload_interval, ) from litellm.proxy.common_utils.proxy_state import ProxyState from litellm.proxy.common_utils.reset_budget_job import ResetBudgetJob from litellm.proxy.common_utils.responses_stream_errors import ResponsesStreamErrorState from litellm.proxy.common_utils.scheduled_job_stagger import ( apply_scheduled_job_stagger, attach_job_timing_logger, parse_stagger_settings, stagger_trigger, ) from litellm.proxy.common_utils.swagger_utils import ERROR_RESPONSES from litellm.proxy.common_utils.timezone_utils import ( get_budget_reset_settings, get_budget_reset_time, ) from litellm.proxy.common_utils.user_api_key_cache import ( UserApiKeyCache, end_user_cache_key, get_management_object_ttl, model_access_group_cache_key, model_access_group_spend_counter_key, project_cache_key, project_spend_counter_key, tag_cache_key, ) from litellm.proxy.config_resolvers import ( FieldSource, SettingsStore, config_ownership_message, resolve_fields, source_for, ) from litellm.proxy.config_resolvers.alerting import ( EMAIL_DESCRIPTORS, MS_TEAMS_DESCRIPTORS, SLACK_DESCRIPTORS, ) from litellm.proxy.config_resolvers.changed_section_keys import changed_section_keys from litellm.proxy.config_resolvers.settings_rules import ( DbRow, Section, coerce_bool, ) from litellm.proxy.config_resolvers.settings_rules import ( JsonValue as SettingsJsonValue, ) from litellm.proxy.container_endpoints.endpoints import router as container_router from litellm.proxy.credential_endpoints.endpoints import router as credential_router from litellm.proxy.db.create_views import SupportsRawQueries from litellm.proxy.db.db_transaction_queue.pod_lock_manager import PodLockManager from litellm.proxy.db.db_transaction_queue.spend_log_cleanup import SpendLogCleanup from litellm.proxy.db.db_transaction_queue.window_spend_update_queue import ( build_window_spend_transaction, ) from litellm.proxy.db.exception_handler import ( PrismaDBExceptionHandler, call_with_db_reconnect_retry, ) from litellm.proxy.db.gateway_request_tracking import ( GatewayRequestAccumulator, GatewayRequestRedisBuffer, flush_gateway_requests, ) from litellm.proxy.db.master_key_migration import ( count_values_encrypted_with_or_none, migrate_if_requested, ) from litellm.proxy.db.proxy_worker_heartbeat import ( PROXY_WORKER_HEARTBEAT_INTERVAL_SECONDS, ProxyWorkerHeartbeat, ) from litellm.proxy.db.spend_counter_reseed import END_USER_COUNTER_PREFIX, SpendCounterReseed from litellm.proxy.discovery_endpoints import ( agent_skills_discovery_router, ui_discovery_endpoints_router, ) from litellm.proxy.fine_tuning_endpoints.endpoints import router as fine_tuning_router from litellm.proxy.fine_tuning_endpoints.endpoints import set_fine_tuning_config from litellm.proxy.google_endpoints.endpoints import router as google_router from litellm.proxy.guardrails.init_guardrails import ( init_guardrails_v2, initialize_guardrails, ) from litellm.proxy.health_check import ( filter_deployments_to_model_groups, health_check_filter_kwargs_from_general_settings, parse_background_health_check_model_groups, perform_health_check, ) from litellm.proxy.health_endpoints._health_endpoints import router as health_router from litellm.proxy.hooks.model_max_budget_limiter import ( _PROXY_VirtualKeyModelMaxBudgetLimiter, ) from litellm.proxy.hooks.prompt_injection_detection import ( _OPTIONAL_PromptInjectionDetection, ) from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger, run_spend_event from litellm.proxy.image_endpoints.endpoints import router as image_router from litellm.proxy.list_api.common import ( ManagementProblem, ValidationErrorDetail, problem_response, request_validation_problem, ) from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request from litellm.proxy.logging_endpoints.callback_logs_endpoints import ( rust_control_plane_router, ) from litellm.proxy.management_endpoints.auto_router_endpoints import ( router as auto_router_management_router, ) from litellm.proxy.management_endpoints.budget_management_endpoints import ( router as budget_management_router, ) from litellm.proxy.management_endpoints.cache_settings_endpoints import ( router as cache_settings_router, ) from litellm.proxy.management_endpoints.callback_management_endpoints import ( router as callback_management_endpoints_router, ) from litellm.proxy.management_endpoints.common_utils import ( _user_has_admin_privileges, _user_has_admin_view, admin_can_invite_user, ) from litellm.proxy.management_endpoints.coordination_redis_endpoints import ( get_persisted_coordination_redis_settings, ) from litellm.proxy.management_endpoints.coordination_redis_endpoints import ( router as coordination_redis_settings_router, ) from litellm.proxy.management_endpoints.cost_tracking_settings import ( router as cost_tracking_settings_router, ) from litellm.proxy.management_endpoints.customer_endpoints import ( router as customer_router, ) from litellm.proxy.management_endpoints.fallback_management_endpoints import ( router as fallback_management_router, ) from litellm.proxy.management_endpoints.gateway_request_endpoints import ( router as gateway_request_router, ) from litellm.proxy.management_endpoints.internal_user_endpoints import ( router as internal_user_router, ) from litellm.proxy.management_endpoints.internal_user_endpoints import ( user_update, ) from litellm.proxy.management_endpoints.key_management_endpoints import ( delete_verification_tokens, duration_in_seconds, generate_key_helper_fn, ) from litellm.proxy.management_endpoints.key_management_endpoints import ( router as key_management_router, ) from litellm.proxy.management_endpoints.management_v1 import ( router as management_v1_router, ) from litellm.proxy.management_endpoints.management_v1.common import MANAGEMENT_V1_PREFIX from litellm.proxy.management_endpoints.model_access_group_management_endpoints import ( router as model_access_group_management_router, ) from litellm.proxy.management_endpoints.model_management_endpoints import ( _add_model_to_db, _add_team_model_to_db, _deduplicate_litellm_router_models, live_model_ids_snapshot, ) from litellm.proxy.management_endpoints.model_management_endpoints import ( router as model_management_router, ) from litellm.proxy.management_endpoints.organization_endpoints import ( router as organization_router, ) from litellm.proxy.management_endpoints.password_endpoints import ( router as password_management_router, ) from litellm.proxy.management_endpoints.prompt_caching_requests import ( router as prompt_caching_requests_router, ) from litellm.proxy.management_endpoints.router_settings_endpoints import ( router as router_settings_router, ) from litellm.proxy.management_endpoints.tag_management_endpoints import ( router as tag_management_router, ) from litellm.proxy.management_endpoints.team_callback_endpoints import ( router as team_callback_router, ) from litellm.proxy.management_endpoints.team_endpoints import router as team_router from litellm.proxy.management_endpoints.team_endpoints import ( update_team, validate_membership, ) from litellm.proxy.management_endpoints.ui_sso import ( get_disabled_non_admin_personal_key_creation, ) from litellm.proxy.management_endpoints.ui_sso import router as ui_sso_router from litellm.proxy.management_endpoints.user_agent_analytics_endpoints import ( router as user_agent_analytics_router, ) from litellm.proxy.management_endpoints.workflow_management_endpoints import ( router as workflow_management_router, ) from litellm.proxy.management_helpers.audit_logs import ( create_audit_log_for_update, create_object_audit_log, ) from litellm.proxy.management_helpers.team_metadata_validation import ( TEAM_METADATA_SCHEMA_REGISTRY, TEAM_METADATA_VALIDATOR_REGISTRY, parse_team_metadata_schema, ) from litellm.proxy.memory.memory_endpoints import router as memory_router from litellm.proxy.middleware.billable_request_metrics_middleware import ( BillableRequestMetricsMiddleware, BillingRecorder, ) from litellm.proxy.middleware.budget_reservation_release_middleware import ( BudgetReservationReleaseMiddleware, ) from litellm.proxy.plugin_routes import ( register_plugins_from_config, ) from litellm.proxy.plugin_routes import ( router as plugin_router, ) from litellm.proxy.spend_tracking.spend_event_producer import ( CollectorSettings, SpendEventProducer, build_spend_event_producer, ) try: from litellm.proxy.enterprise_billing.billing_metrics import ( build_billing_metrics_recorder as _build_billing_metrics_recorder, ) from litellm.proxy.enterprise_billing.billing_metrics import ( shutdown_billing_metrics_recorder as _shutdown_billing_metrics_recorder, ) build_billing_metrics_recorder: Callable[..., BillingRecorder | None] | None = _build_billing_metrics_recorder shutdown_billing_metrics_recorder: Callable[[], None] | None = _shutdown_billing_metrics_recorder except ImportError: build_billing_metrics_recorder = None shutdown_billing_metrics_recorder = None from litellm.proxy.middleware.admission_control_middleware import ( AdmissionControlMiddleware, admission_control_state, get_admission_control_settings, ) from litellm.proxy.middleware.in_flight_requests_middleware import ( InFlightRequestsMiddleware, ) from litellm.proxy.middleware.per_request_root_path_middleware import ( PerRequestRootPathMiddleware, get_server_root_paths, ) from litellm.proxy.middleware.prometheus_auth_middleware import PrometheusAuthMiddleware from litellm.proxy.middleware.request_size_limit_middleware import ( RequestSizeLimitMiddleware, ) from litellm.proxy.middleware.security_headers_middleware import ( SecurityHeadersMiddleware, ) from litellm.proxy.ocr_endpoints.endpoints import router as ocr_router from litellm.proxy.openai_files_endpoints.files_endpoints import ( router as openai_files_router, ) from litellm.proxy.openai_files_endpoints.files_endpoints import ( set_files_config, ) from litellm.proxy.pass_through_endpoints.openai_passthrough_endpoints import ( router as openai_passthrough_router, ) from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( initialize_pass_through_endpoints, ) from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( router as pass_through_router, ) from litellm.proxy.prometheus_cleanup import mark_dead_workers, mark_worker_exit from litellm.proxy.public_endpoints import router as public_endpoints_router from litellm.proxy.public_endpoints.public_v1 import router as public_v1_router from litellm.proxy.rag_endpoints.endpoints import router as rag_router from litellm.proxy.rerank_endpoints.endpoints import router as rerank_router from litellm.proxy.response_api_endpoints.endpoints import router as response_router from litellm.proxy.route_llm_request import route_request from litellm.proxy.route_priority import hot_routes_first from litellm.proxy.search_endpoints.endpoints import router as search_router from litellm.proxy.shutdown.graceful_shutdown_manager import GracefulShutdownManager from litellm.proxy.shutdown.scheduled_jobs import ( AwaitableAsyncIOExecutor, pause_scheduled_jobs, stop_in_flight_scheduler_jobs, ) from litellm.proxy.spend_tracking.budget_reservation import ( get_budget_window_start, release_unbound_budget_reservation, ) from litellm.proxy.spend_tracking.daily_global_spend_rollup import ( run_scheduled_daily_global_spend_reconcile, ) from litellm.proxy.spend_tracking.spend_counter_batch import ( PendingSpendIncrement, active_spend_counter_batch, forget_spend_counter, post_call_counter_keys, read_batched_spend_counter, record_spend_counter_value, spend_counter_batch_scope, ) from litellm.proxy.spend_tracking.spend_management_endpoints import ( router as spend_management_router, ) from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload from litellm.proxy.types_utils.utils import get_instance_fn from litellm.proxy.ui_crud_endpoints.latest_release_endpoints import ( router as latest_release_endpoints_router, ) from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( router as ui_crud_endpoints_router, ) from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( sync_ui_settings_to_general_settings, ) from litellm.proxy.ui_crud_endpoints.user_banner_endpoints import ( router as user_banner_endpoints_router, ) from litellm.proxy.utils import ( PrismaClient, ProxyLogging, ProxyUpdateSpend, _cache_user_row, _get_docs_url, _get_openapi_url, _get_projected_spend_over_limit, _get_redoc_url, _is_projected_spend_over_limit, _is_valid_team_configs, evict_config_param, get_config_param, get_custom_url, get_error_message_str, get_server_root_path, handle_exception_on_proxy, hash_password, hash_token, invalidate_config_param, litellm_config_cache, migrate_passwords_to_scrypt_async, model_dump_with_preserved_fields, prefetch_config_params, update_spend, ) from litellm.proxy.video_endpoints.endpoints import router as video_router from litellm.repositories.base_repository import SupportsModelDump from litellm.repositories.credentials_repository import CredentialsRepository from litellm.repositories.prisma_protocols import TableActions from litellm.router import ( AssistantsTypedDict, Deployment, LiteLLM_Params, ModelGroupInfo, ) from litellm.scheduler import FlowItem, Scheduler from litellm.secret_managers.aws_secret_manager import load_aws_kms from litellm.secret_managers.google_kms import load_google_kms from litellm.secret_managers.main import ( get_secret, get_secret_bool, get_secret_str, normalize_nonempty_secret_str, secret_manager_would_be_consulted, str_to_bool, ) from litellm.types.integrations.slack_alerting import AlertType, SlackAlertingArgs from litellm.types.llms.anthropic import ( AnthropicMessagesRequest, AnthropicResponse, AnthropicResponseContentBlockText, AnthropicResponseUsageBlock, ) from litellm.types.llms.openai import ( AllMessageValues, ChatCompletionSystemMessage, ChatCompletionToolParam, HttpxBinaryResponseContent, ) from litellm.types.proxy.control_plane_endpoints import WorkerRegistryEntry from litellm.types.proxy.management_endpoints.model_management_endpoints import ( ModelGroupInfoProxy, ) from litellm.types.proxy.management_endpoints.ui_sso import ( DefaultTeamSSOParams, LiteLLM_UpperboundKeyGenerateParams, ) from litellm.types.proxy.model_deprecation import ( DEFAULT_DEPRECATION_WARN_DAYS, ModelDeprecationResponse, ) from litellm.types.realtime import RealtimeQueryParams from litellm.types.router import ( ClassifierPlugin, DeploymentTypedDict, RouterGeneralSettings, RoutingGroup, RoutingPlugin, SearchToolTypedDict, updateDeployment, ) from litellm.types.router import ModelInfo as RouterModelInfo from litellm.types.scheduler import DefaultPriorities from litellm.types.secret_managers.main import ( KeyManagementSettings, KeyManagementSystem, ) from litellm.types.utils import CredentialItem, CustomHuggingfaceTokenizer, RawRequestTypedDict, StandardLoggingPayload from litellm.types.utils import ModelInfo as ModelMapInfo from litellm.utils import _add_custom_logger_callback_to_specific_event try: from litellm._version import version except Exception: version = "0.0.0" litellm.suppress_debug_info = True import json from fastapi import ( Depends, FastAPI, File, Form, Header, HTTPException, Path, Query, Request, Response, UploadFile, WebSocket, WebSocketDisconnect, applications, status, ) from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.docs import get_swagger_ui_html from fastapi.openapi.utils import get_openapi from fastapi.responses import ( FileResponse, HTMLResponse, JSONResponse, ORJSONResponse, RedirectResponse, StreamingResponse, ) from fastapi.routing import APIRouter from fastapi.security import OAuth2PasswordBearer from fastapi.security.api_key import APIKeyHeader from fastapi.staticfiles import StaticFiles # import enterprise folder enterprise_router = APIRouter() try: # when using litellm cli from litellm.proxy import enterprise except Exception: # when using litellm docker image try: import enterprise except Exception: pass ################### # Import enterprise routes try: from litellm_enterprise.proxy.enterprise_routes import router as _enterprise_router from litellm_enterprise.proxy.proxy_server import EnterpriseProxyConfig enterprise_router = _enterprise_router enterprise_proxy_config: EnterpriseProxyConfig | None = EnterpriseProxyConfig() except ImportError: enterprise_proxy_config = None ################### server_root_path: Final = get_server_root_path() _license_check = LicenseCheck() premium_user: bool = _license_check.is_premium() premium_user_data: Optional["EnterpriseLicenseData"] = _license_check.airgapped_license_data global_max_parallel_request_retries_env: Final[str | None] = os.getenv("LITELLM_GLOBAL_MAX_PARALLEL_REQUEST_RETRIES") proxy_state: Final = ProxyState() SENSITIVE_DATA_MASKER: Final = SensitiveDataMasker() # Secret-bearing general_settings fields the segment masker does not match by # name: database_url and database_extra_connection_params embed DB credentials, # pass_through_endpoints carry upstream Authorization headers, and # alert_to_webhook_url is itself a webhook secret _EXTRA_SECRET_GENERAL_SETTINGS_FIELDS: Final = frozenset( { "database_url", "database_extra_connection_params", "pass_through_endpoints", "alert_to_webhook_url", } ) def _redact_worker_config_for_logging(worker_config: str | dict[str, JsonValue] | None) -> JsonValue: """Mask sensitive fields in the worker config before it enters a log record. `worker_config` reaches `proxy_startup_event` as either the JSON blob persisted by `save_worker_config` (a string) or the dict passed directly to `initialize`. Both shapes can carry `master_key`, `database_url`, provider API keys, etc.; passing the raw value to `verbose_proxy_logger` leaks them whenever the last-line-of-defense regex filter is bypassed (`LITELLM_DISABLE_REDACT_SECRETS=true`, an older log sink, a downstream handler that captures records pre-filter). Redact at the source. """ if worker_config is None: return None if isinstance(worker_config, dict): return _redact_secret_values_in_obj(worker_config) parsed: Final = safe_json_loads(worker_config, default=None) if isinstance(parsed, dict): return safe_dumps(_redact_secret_values_in_obj(parsed)) return worker_config if global_max_parallel_request_retries_env is None: global_max_parallel_request_retries: int = 3 else: global_max_parallel_request_retries = int(global_max_parallel_request_retries_env) global_max_parallel_request_retry_timeout_env: Final[str | None] = os.getenv( "LITELLM_GLOBAL_MAX_PARALLEL_REQUEST_RETRY_TIMEOUT" ) if global_max_parallel_request_retry_timeout_env is None: global_max_parallel_request_retry_timeout: float = 60.0 else: global_max_parallel_request_retry_timeout = float(global_max_parallel_request_retry_timeout_env) ui_link: Final = f"{server_root_path}/ui" fallback_login_link: Final = f"{server_root_path}/fallback/login" model_hub_link: Final = f"{server_root_path}/ui/model_hub_table" ui_message = f"šŸ‘‰ [```LiteLLM Admin Panel on /ui```]({ui_link}). Create, Edit Keys with SSO. Having issues? Try [```Fallback Login```]({fallback_login_link})" ui_message += "\n\nšŸ’ø [```LiteLLM Model Cost Map```](https://models.litellm.ai/)." ui_message += f"\n\nšŸ”Ž [```LiteLLM Model Hub```]({model_hub_link}). See available models on the proxy. [**Docs**](https://docs.litellm.ai/docs/proxy/ai_hub)" custom_swagger_message: Final = ( "[**Customize Swagger Docs**](https://docs.litellm.ai/docs/proxy/enterprise#swagger-docs---custom-routes--branding)" ) ### CUSTOM BRANDING [ENTERPRISE FEATURE] ### _title: Final = os.getenv("DOCS_TITLE", "LiteLLM API") if premium_user else "LiteLLM API" _description: Final = ( os.getenv( "DOCS_DESCRIPTION", f"Enterprise Edition \n\nProxy Server to call 100+ LLMs in the OpenAI format. {custom_swagger_message}\n\n{ui_message}", ) if premium_user else f"Proxy Server to call 100+ LLMs in the OpenAI format. {custom_swagger_message}\n\n{ui_message}" ) def cleanup_router_config_variables(): global \ master_key, \ user_config_file_path, \ otel_logging, \ user_custom_auth, \ user_custom_auth_path, \ user_custom_key_generate, \ user_custom_key_update, \ user_custom_key_policy, \ user_custom_sso, \ user_custom_ui_sso_sign_in_handler, \ use_background_health_checks, \ use_shared_health_check, \ health_check_interval, \ health_check_concurrency, \ prisma_client, \ heuristic_v1_tuning_baselines # Set all variables to None master_key = None user_config_file_path = None otel_logging = None user_custom_auth = None user_custom_auth_path = None user_custom_key_generate = None user_custom_key_update = None user_custom_key_policy = None TEAM_METADATA_VALIDATOR_REGISTRY.set(None) TEAM_METADATA_SCHEMA_REGISTRY.set(()) user_custom_sso = None user_custom_ui_sso_sign_in_handler = None use_background_health_checks = None use_shared_health_check = None health_check_interval = None health_check_concurrency = None prisma_client = None heuristic_v1_tuning_baselines = None async def flush_spend_counters_on_shutdown() -> None: if prisma_client is None: return try: await proxy_logging_obj.db_spend_update_writer.db_update_spend_transaction_handler( prisma_client=prisma_client, n_retry_times=3, proxy_logging_obj=proxy_logging_obj ) except Exception as e: # noqa: BLE001 # shutdown must continue even if the commit fails verbose_proxy_logger.exception("Error flushing spend counters on shutdown: %s", e) async def _flush_spend_logs_queue_on_shutdown() -> None: if prisma_client is None: return try: from litellm.proxy.utils import drain_spend_logs_queue await drain_spend_logs_queue( prisma_client=prisma_client, db_writer_client=db_writer_client, proxy_logging_obj=proxy_logging_obj, ) except Exception as e: # noqa: BLE001 # shutdown must continue even if the drain fails verbose_proxy_logger.exception("Error flushing spend logs queue on shutdown: %s", e) async def proxy_shutdown_event(worker_heartbeat: ProxyWorkerHeartbeat | None = None) -> None: global prisma_client, master_key, user_custom_auth, user_custom_key_generate, user_custom_key_update verbose_proxy_logger.info("Shutting down LiteLLM Proxy Server") if worker_heartbeat is not None and prisma_client: await worker_heartbeat.deregister() if prisma_client: # Drain the SGR fold first: it lives in memory, so an un-drained interval # is lost, and a write attempted after disconnect raises # ClientNotConnectedError rather than persisting anything. Ordering this # inside the same guard is what keeps the two from drifting apart. await flush_gateway_requests(prisma_client, gateway_request_accumulator) verbose_proxy_logger.debug("Disconnecting from Prisma") await prisma_client.disconnect() if litellm.cache is not None: await litellm.cache.disconnect() await jwt_handler.close() if db_writer_client is not None: await db_writer_client.close() # final flush of billable-request counts: without it, up to one export # interval of enterprise billing data is dropped on every restart if shutdown_billing_metrics_recorder is not None: shutdown_billing_metrics_recorder() # flush remaining langfuse logs if "langfuse" in litellm.success_callback: try: # flush langfuse logs on shutdow from litellm.utils import langFuseLogger if langFuseLogger is not None: langFuseLogger.Langfuse.flush() except Exception: # [DO NOT BLOCK shutdown events for this] pass ## RESET CUSTOM VARIABLES ## cleanup_router_config_variables() _AiohttpAddrInfo: TypeAlias = tuple[int | socket.AddressFamily, int | socket.SocketKind, int, str, tuple[object, ...]] class _AiohttpConnectorKwargs(TypedDict, total=False): keepalive_timeout: float ttl_dns_cache: int enable_cleanup_closed: bool limit: int limit_per_host: int socket_factory: Callable[[_AiohttpAddrInfo], socket.socket] async def _initialize_shared_aiohttp_session(): """Initialize shared aiohttp session for connection reuse with connection limits.""" try: from aiohttp import ClientSession, DummyCookieJar, TCPConnector from litellm.llms.custom_httpx.http_handler import ( _build_aiohttp_keepalive_socket_factory, ) connector_kwargs: Final[_AiohttpConnectorKwargs] = { "keepalive_timeout": AIOHTTP_KEEPALIVE_TIMEOUT, "ttl_dns_cache": AIOHTTP_TTL_DNS_CACHE, } if AIOHTTP_NEEDS_CLEANUP_CLOSED: connector_kwargs["enable_cleanup_closed"] = True if AIOHTTP_CONNECTOR_LIMIT > 0: connector_kwargs["limit"] = AIOHTTP_CONNECTOR_LIMIT if AIOHTTP_CONNECTOR_LIMIT_PER_HOST > 0: connector_kwargs["limit_per_host"] = AIOHTTP_CONNECTOR_LIMIT_PER_HOST socket_factory: Final = _build_aiohttp_keepalive_socket_factory() if socket_factory is not None: connector_kwargs["socket_factory"] = socket_factory connector: Final = TCPConnector(**connector_kwargs) session: Final = ClientSession(connector=connector, cookie_jar=DummyCookieJar()) verbose_proxy_logger.info( "SESSION REUSE: Created shared aiohttp session for connection pooling (ID: %s, limit=%s, limit_per_host=%s)", id(session), AIOHTTP_CONNECTOR_LIMIT, AIOHTTP_CONNECTOR_LIMIT_PER_HOST, ) return session except Exception as e: verbose_proxy_logger.warning( "Failed to create shared aiohttp session: %s. Continuing without session reuse.", e ) return None async def _connect_to_count_stored_values() -> SupportsRawQueries: client: Final = prisma_client or PrismaClient( database_url=str(get_secret("DATABASE_URL")), proxy_logging_obj=proxy_logging_obj ) await client.connect() return client.writer_db @asynccontextmanager async def proxy_startup_event(app: FastAPI) -> AsyncGenerator[None, None]: global \ prisma_client, \ master_key, \ use_background_health_checks, \ llm_router, \ llm_model_list, \ general_settings, \ proxy_budget_rescheduler_min_time, \ proxy_budget_rescheduler_max_time, \ litellm_proxy_admin_name, \ db_writer_client, \ store_model_in_db, \ premium_user, \ _license_check, \ proxy_batch_polling_interval, \ shared_aiohttp_session import json init_verbose_loggers() prometheus_multiproc_dir: Final = os.environ.get("PROMETHEUS_MULTIPROC_DIR") if prometheus_multiproc_dir: mark_dead_workers(prometheus_multiproc_dir) ## RUN WORKER STARTUP HOOKS (e.g., gflags initialization) ## _startup_hooks_env: Final = os.environ.get("LITELLM_WORKER_STARTUP_HOOKS", "") if _startup_hooks_env: for _hook_spec in _startup_hooks_env.split(","): _hook_spec = _hook_spec.strip() if not _hook_spec: continue try: if ":" not in _hook_spec: raise ValueError( f"Invalid hook spec '{_hook_spec}': expected format is 'module.path:function_name'" ) _module_path, _func_name = _hook_spec.rsplit(":", 1) _module = importlib.import_module(_module_path) _hook_fn = getattr(_module, _func_name) if inspect.iscoroutinefunction(_hook_fn): await _hook_fn() else: _hook_fn() verbose_proxy_logger.info("Worker startup hook '%s' executed successfully", _hook_spec) except Exception as e: verbose_proxy_logger.error("Worker startup hook '%s' failed: %s", _hook_spec, e) raise ## CHECK PREMIUM USER verbose_proxy_logger.debug("litellm.proxy.proxy_server.py::startup() - CHECKING PREMIUM USER - %s", premium_user) if premium_user is False: premium_user = _license_check.is_premium() ## CHECK MASTER KEY IN ENVIRONMENT ## master_key = get_secret_str("LITELLM_MASTER_KEY") ### LOAD CONFIG ### worker_config: str | dict | None = get_secret("WORKER_CONFIG") env_config_yaml: Final[str | None] = get_secret_str("CONFIG_FILE_PATH") verbose_proxy_logger.debug("worker_config: %s", _redact_worker_config_for_logging(worker_config)) # check if it's a valid file path if env_config_yaml is not None: if os.path.isfile(env_config_yaml) and proxy_config.is_yaml(config_file_path=env_config_yaml): ( llm_router, llm_model_list, general_settings, ) = await proxy_config.load_config(router=llm_router, config_file_path=env_config_yaml) elif worker_config is not None: if ( ( isinstance(worker_config, str) and os.path.isfile(worker_config) and proxy_config.is_yaml(config_file_path=worker_config) ) or os.environ.get("LITELLM_CONFIG_BUCKET_NAME") is not None and isinstance(worker_config, str) ): ( llm_router, llm_model_list, general_settings, ) = await proxy_config.load_config(router=llm_router, config_file_path=worker_config) elif isinstance(worker_config, dict): await initialize_from_worker_config(worker_config) else: # if not, assume it's a json string worker_config = json.loads(worker_config) if isinstance(worker_config, dict): await initialize_from_worker_config(worker_config) enforce_master_key_boot_verdict( await with_stored_secrets_counted( master_key_boot_verdict( master_key=master_key, environment_master_key=os.getenv(MASTER_KEY_ENV_VAR), general_settings=general_settings, config_file_path=user_config_file_path, override_env_is_on=get_secret_bool(WEAK_OR_UNSET_MASTER_KEY_OVERRIDE_ENV_VAR) is True, salt_key_is_set=os.getenv(SALT_KEY_ENV_VAR) is not None, database_is_configured=prisma_client is not None or get_secret("DATABASE_URL", None) is not None, ), count_values_encrypted_with=partial(count_values_encrypted_with_or_none, _connect_to_count_stored_values), ), announce=announce_on_stderr_at_exit, ) # check if DATABASE_URL in environment - load from there if prisma_client is None: _db_url: Final[str | None] = get_secret("DATABASE_URL", None) prisma_client = await ProxyStartupEvent._setup_prisma_client( database_url=_db_url, proxy_logging_obj=proxy_logging_obj, user_api_key_cache=user_api_key_cache, ) await migrate_if_requested( environ=os.environ, master_key=master_key, connected_database=lambda: None if prisma_client is None else prisma_client.writer_db, log=verbose_proxy_logger.warning, raise_unless_tolerated=PrismaDBExceptionHandler.handle_db_exception, ) if prisma_client is not None: async def _run_pw_migration(): try: result: Final = await migrate_passwords_to_scrypt_async(prisma_client) verbose_proxy_logger.info("Password migration: %s", result) except Exception as e: verbose_proxy_logger.warning("Password migration skipped: %s", e) asyncio.create_task(_run_pw_migration()) async def _run_agent_grant_id_migration() -> None: from litellm.proxy.agent_endpoints.agent_registry import ( global_agent_registry, object_permission_table, ) for attempt in range(3): try: result = await global_agent_registry.migrate_legacy_grant_ids( table=object_permission_table(prisma_client) ) if result.rewritten: verbose_proxy_logger.info( "Rewrote %s object_permission rows from legacy config agent ids", result.rewritten ) if result.missed == 0: return verbose_proxy_logger.warning( "Legacy agent grant id migration attempt %s/3 left %s rows unmigrated", attempt + 1, result.missed, ) except Exception as e: # noqa: BLE001 # startup task must survive any DB error and retry verbose_proxy_logger.warning( "Legacy agent grant id migration attempt %s/3 failed: %s", attempt + 1, e ) if attempt < 2: await asyncio.sleep(5) asyncio.create_task(_run_agent_grant_id_migration()) ## A coordination_redis block saved from the admin UI lives in the database, ## which is only reachable once the prisma client exists. Apply it here, before ## the coordination Redis is published to its consumers below. db_coordination_redis_cache: Final = await ProxyStartupEvent._init_coordination_redis_from_db( litellm_settings=proxy_config.get_config_state().get("litellm_settings") or {}, llm_router=llm_router, ) if db_coordination_redis_cache is not None: _set_redis_usage_cache(db_coordination_redis_cache) ## use_redis_transaction_buffer: fall back to a standalone Redis (REDIS_* env) ## when the proxy cache backend is not Redis ## transaction_buffer_redis_cache = redis_usage_cache if transaction_buffer_redis_cache is None: transaction_buffer_redis_cache = ProxyStartupEvent._get_transaction_buffer_redis_cache( general_settings=general_settings ) ProxyStartupEvent._initialize_startup_logging( llm_router=llm_router, proxy_logging_obj=proxy_logging_obj, redis_usage_cache=transaction_buffer_redis_cache, ) ## V2 OTEL: publish the chosen V2 logger's TracerProvider as the OTel global. ## This MUST run after callback initialization above: a preset (arize, langfuse, ## …) builds its logger there, folding the OTEL_* base exporter and its own ## exporter into one logger. The FastAPI instrumentation mounted at app-creation ## binds to the global provider, so reusing that one logger is what makes the ## server span and the gen-ai spans share one provider and land in the same ## trace, exporting to every configured backend. Running before callback init ## (when no logger exists yet) would build a second, generic logger whose ## provider became the global, orphaning the gen-ai spans onto a different ## backend than the server span. A generic logger is built only when none was ## configured. try: from litellm.integrations.otel.model.config import is_otel_v2_enabled if is_otel_v2_enabled(): from opentelemetry import trace as _otel_trace from litellm.integrations.otel.logger import ( OpenTelemetryV2, publish_global_otel_v2_provider, ) from litellm.litellm_core_utils.litellm_logging import _in_memory_loggers registered: Final = open_telemetry_logger if isinstance(open_telemetry_logger, OpenTelemetryV2) else None publish_global_otel_v2_provider( _in_memory_loggers, # any-ok: pre-existing untyped List[Any] global _otel_trace.set_tracer_provider, registered=registered, ) except Exception as e: verbose_proxy_logger.debug("Skipping OTel V2 provider setup: %s", e) ## Validate use_redis_transaction_buffer requires Redis cache ## ProxyStartupEvent._validate_redis_transaction_buffer_config( general_settings=general_settings, redis_usage_cache=transaction_buffer_redis_cache, ) ProxyStartupEvent._warn_if_mock_testing_params_enabled(general_settings=general_settings) ## SEMANTIC TOOL FILTER ## # Read litellm_settings from config for semantic filter initialization try: verbose_proxy_logger.debug("About to initialize semantic tool filter") _config: Final = proxy_config.get_config_state() _litellm_settings: Final = _config.get("litellm_settings", {}) verbose_proxy_logger.debug("litellm_settings keys = %s", list(_litellm_settings.keys())) await ProxyStartupEvent._initialize_semantic_tool_filter( llm_router=llm_router, litellm_settings=_litellm_settings, ) verbose_proxy_logger.debug("After semantic tool filter initialization") except Exception as e: verbose_proxy_logger.error("Semantic filter init failed: %s", e, exc_info=True) ## JWT AUTH ## ProxyStartupEvent._initialize_jwt_auth( general_settings=general_settings, prisma_client=prisma_client, user_api_key_cache=user_api_key_cache, ) ProxyStartupEvent._attach_router_to_prompt_injection_detectors(llm_router=llm_router) verbose_proxy_logger.debug("prisma_client: %s", prisma_client) if prisma_client is not None and litellm.max_budget > 0: ProxyStartupEvent._add_proxy_budget_to_db() asyncio.create_task( ProxyStartupEvent._warm_global_spend_cache( user_api_key_cache=user_api_key_cache, prisma_client=prisma_client, ) ) ProxyStartupEvent._warn_budget_without_db( max_budget=litellm.max_budget, prisma_client=prisma_client, ) ### START BATCH WRITING DB + CHECKING NEW MODELS### worker_heartbeat: Final = ( await ProxyStartupEvent.initialize_scheduled_background_jobs( general_settings=general_settings, prisma_client=prisma_client, proxy_budget_rescheduler_min_time=proxy_budget_rescheduler_min_time, proxy_budget_rescheduler_max_time=proxy_budget_rescheduler_max_time, proxy_batch_write_at=proxy_batch_write_at, proxy_logging_obj=proxy_logging_obj, ) if prisma_client is not None else None ) if prisma_client is not None: await ProxyStartupEvent._update_default_team_member_budget() ## SYNC UI SETTINGS ## await ProxyStartupEvent._sync_ui_settings_to_general_settings() # Start background health checks AFTER models are loaded and index is built if use_background_health_checks: asyncio.create_task(_run_background_health_check()) # start the background health check coroutine. # Start adaptive-router queue flusher unconditionally — adaptive routers # may be added later via `/config/reload`, and the flusher is a no-op when # `llm_router.adaptive_routers` is empty. Per-router DB state is loaded # lazily by the flusher on first tick (see `_state_loaded` flag) so # hot-reloaded routers also get their persisted priors. if llm_router is not None and getattr(llm_router, "adaptive_routers", None): for _tagged_routers in llm_router.adaptive_routers.values(): for _tagged in _tagged_routers: await _tagged.strategy.load_state_from_db(prisma_client) _tagged.strategy._state_loaded = True asyncio.create_task(_adaptive_router_flusher_loop()) ## [Optional] Initialize dd tracer ProxyStartupEvent._init_dd_tracer() ## [Optional] Initialize Pyroscope continuous profiling (env: LITELLM_ENABLE_PYROSCOPE=true) ProxyStartupEvent._init_pyroscope() ## Initialize shared aiohttp session for connection reuse shared_aiohttp_session = await _initialize_shared_aiohttp_session() model_info_refresh_disabled: Final = ( "disable_model_info_refresh" in general_settings and general_settings["disable_model_info_refresh"] is True ) model_info_scheduler: Final = ( None if model_info_refresh_disabled else scheduler if scheduler is not None else AsyncIOScheduler() ) if model_info_scheduler is not None: model_info_scheduler.add_job( ProxyStartupEvent.refresh_model_info, "interval", seconds=MODEL_INFO_REFRESH_SECONDS, id="refresh_model_info", next_run_time=datetime.now(timezone.utc), max_instances=1, replace_existing=True, ) if not model_info_scheduler.running: model_info_scheduler.start() # End of startup event yield if model_info_scheduler is not None and model_info_scheduler.running: model_info_scheduler.remove_job("refresh_model_info") if model_info_scheduler is not scheduler: model_info_scheduler.shutdown(wait=False) # Shutdown event - stop starting scheduled jobs; the ones already running keep the drain window if scheduler is not None: pause_scheduled_jobs(scheduler) # Shutdown event - drain in-flight requests before tearing down dependencies # so SIGTERM (rolling update, scale-down, liveness kill) doesn't drop them. GracefulShutdownManager.start_shutdown() await GracefulShutdownManager.wait_for_drain() # Shutdown event - close shared aiohttp session if shared_aiohttp_session is not None: try: await shared_aiohttp_session.close() verbose_proxy_logger.info("SESSION REUSE: Closed shared aiohttp session") except Exception as e: verbose_proxy_logger.error("Error closing shared aiohttp session: %s", e) # Shutdown event - stop RDS IAM token refresh background task if ( prisma_client is not None and hasattr(prisma_client, "db") and hasattr(prisma_client.db, "stop_token_refresh_task") ): try: await prisma_client.db.stop_token_refresh_task() except Exception as e: verbose_proxy_logger.error("Error stopping token refresh task: %s", e) # Shutdown event - stop Prisma DB health watchdog task if prisma_client is not None and hasattr(prisma_client, "stop_db_health_watchdog_task"): try: await prisma_client.stop_db_health_watchdog_task() except Exception as e: verbose_proxy_logger.error("Error stopping DB health watchdog task: %s", e) if prisma_client is not None and hasattr(prisma_client, "stop_view_setup_task"): try: await prisma_client.stop_view_setup_task() except Exception as e: verbose_proxy_logger.error("Error stopping the spend view setup task: %s", e) await _drain_spend_event_producer_on_shutdown() # Shutdown event - finish or cancel in-flight scheduled jobs before the shutdown flushes and the DB disconnect if scheduler is not None and scheduler_executor is not None: try: await stop_in_flight_scheduler_jobs(scheduler, scheduler_executor) except Exception as e: verbose_proxy_logger.error("Error stopping in-flight scheduled jobs: %s", e) await flush_spend_counters_on_shutdown() await _flush_spend_logs_queue_on_shutdown() await proxy_config.stop_config_sync_subscriber() await proxy_config.stop_auth_cache_invalidation_subscriber() await proxy_shutdown_event(worker_heartbeat=worker_heartbeat) if prometheus_multiproc_dir: mark_worker_exit(os.getpid()) def _generate_stable_operation_id(route: "APIRoute") -> str: operation_id = re.sub(r"\W", "_", f"{route.name}{route.path_format}") route_methods: Final = sorted(route.methods or []) if len(route_methods) == 1: operation_id = f"{operation_id}_{route_methods[0].lower()}" return operation_id _OPENAPI_HTTP_METHODS: Final = { "delete", "get", "head", "options", "patch", "post", "put", "trace", } # Credentials surfaced by `/get/config/callbacks` in the alerting block: the # full Slack incoming-webhook URL is itself a credential, and the SMTP # password is a service password. Masked on read so plaintext never reaches # the UI. Kept here at module scope to match the analogous descriptor # `is_secret` flags in litellm.proxy.config_resolvers and the # `_CACHE_SENSITIVE_FIELDS` constant in the cache endpoint file. _ALERTING_SENSITIVE_VARS: Final[set[str]] = {"ALERTING_WEBHOOK_URL", "SLACK_WEBHOOK_URL", "SMTP_PASSWORD"} def _strip_operation_id_method_suffix(operation_id: str) -> str: base, separator, suffix = operation_id.rpartition("_") if separator and suffix in _OPENAPI_HTTP_METHODS: return base return operation_id def ensure_unique_openapi_operation_ids( openapi_schema: dict[str, Any], reserved_operation_ids: set[str] | None = None, ) -> dict[str, Any]: operation_entries: Final = [] operation_id_counts: Final[dict[str, int]] = {} for path_item in openapi_schema.get("paths", {}).values(): if not isinstance(path_item, dict): continue for method, operation in path_item.items(): if method not in _OPENAPI_HTTP_METHODS or not isinstance(operation, dict): continue operation_id = operation.get("operationId") if not isinstance(operation_id, str): continue operation_entries.append((method, operation, operation_id)) operation_id_counts[operation_id] = operation_id_counts.get(operation_id, 0) + 1 used_operation_ids: Final = set(reserved_operation_ids or set()) seen_operation_ids: Final[set[str]] = set() for method, operation, operation_id in operation_entries: should_rewrite = ( operation_id_counts[operation_id] > 1 or operation_id in used_operation_ids or operation_id in seen_operation_ids ) if not should_rewrite: seen_operation_ids.add(operation_id) used_operation_ids.add(operation_id) continue base_operation_id = _strip_operation_id_method_suffix(operation_id) new_operation_id = f"{base_operation_id}_{method}" suffix = 2 while new_operation_id in used_operation_ids or new_operation_id in seen_operation_ids: new_operation_id = f"{base_operation_id}_{method}_{suffix}" suffix += 1 operation["operationId"] = new_operation_id seen_operation_ids.add(new_operation_id) used_operation_ids.add(new_operation_id) if reserved_operation_ids is not None: reserved_operation_ids.update(used_operation_ids) return openapi_schema app = FastAPI( docs_url=_get_docs_url(), redoc_url=_get_redoc_url(), openapi_url=_get_openapi_url(), title=_title, description=_description, version=version, root_path=server_root_path, lifespan=proxy_startup_event, generate_unique_id_function=_generate_stable_operation_id, strict_content_type=False, ) ## V2 OTEL: instrument the FastAPI app for server spans (gated by ## LITELLM_OTEL_V2). This MUST run at app-creation time — once the lifespan runs, ## the middleware stack is frozen and ``instrument_app`` raises "Cannot add ## middleware after an application has started". See ## ``litellm.integrations.otel.mount`` for the full rationale; the call is a safe ## no-op when the gate is off or the instrumentation package is unavailable. from litellm.integrations.otel.mount import instrument_fastapi_app instrument_fastapi_app(app) vertex_live_passthrough_vertex_base: Final = VertexBase() ### CUSTOM API DOCS [ENTERPRISE FEATURE] ### # Custom OpenAPI schema generator to include only selected routes from fastapi.routing import APIWebSocketRoute def _inject_websocket_stubs_into_openapi_schema(openapi_schema: dict, websocket_routes: list) -> dict: """ Add a synthetic GET stub for each WebSocket route so it appears in Swagger UI. Merges into any existing path entry rather than replacing it — a WebSocket route that shares its path with an HTTP route must not erase the HTTP operation. If a "get" operation is already documented on the path, the WebSocket stub is skipped to preserve the real GET. """ for route in websocket_routes: base_path = route.path.split("{")[0].rstrip("?") parameters = [] try: if hasattr(route, "dependant") and route.dependant is not None: # Handle both FastAPI <0.120 and >=0.120 query_params = getattr(route.dependant, "query_params", []) if query_params: for param in query_params: parameters.append( { "name": param.name, "in": "query", "required": param.required, "schema": {"type": "string"}, } ) except (AttributeError, TypeError): pass path_entry = openapi_schema["paths"].setdefault(base_path, {}) if "get" not in path_entry: path_entry["get"] = { "summary": f"WebSocket: {route.name or base_path}", "description": "WebSocket connection endpoint", "operationId": f"websocket_{route.name or base_path.replace('/', '_')}", "parameters": parameters, "responses": {"101": {"description": "WebSocket Protocol Switched"}}, "tags": ["WebSocket"], } return openapi_schema def get_openapi_schema(): if app.openapi_schema: return app.openapi_schema # Use compatibility wrapper for FastAPI 0.120+ schema generation from litellm.proxy.common_utils.openapi_schema_compat import ( get_openapi_schema_with_compat, ) openapi_schema = get_openapi_schema_with_compat( get_openapi_func=get_openapi, title=app.title, version=app.version, description=app.description, routes=app.routes, ) # Find all WebSocket routes websocket_routes: Final = [route for route in app.routes if isinstance(route, APIWebSocketRoute)] # Add a synthetic GET stub for each so they render in Swagger UI, # without clobbering existing HTTP operations on the same path. openapi_schema = _inject_websocket_stubs_into_openapi_schema(openapi_schema, websocket_routes) # Add LLM API request schema bodies for documentation from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(openapi_schema) # Stub unloaded lazy features so they appear as Swagger sections. from litellm.proxy._lazy_features import inject_lazy_stubs, loaded_lazy_modules openapi_schema = inject_lazy_stubs(openapi_schema, loaded_lazy_modules(app)) openapi_schema = ensure_unique_openapi_operation_ids(openapi_schema) # Fix Swagger UI execute path error when server_root_path is set if server_root_path: openapi_schema["servers"] = [{"url": "/" + server_root_path.strip("/")}] app.openapi_schema = openapi_schema return app.openapi_schema def custom_openapi(): if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi_schema() # Filter routes to include only specific ones openai_routes: Final = LiteLLMRoutes.openai_routes.value paths_to_include: Final[dict] = {} for route in openai_routes: if route in openapi_schema["paths"]: paths_to_include[route] = openapi_schema["paths"][route] openapi_schema["paths"] = paths_to_include # Add LLM API request schema bodies for documentation from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(openapi_schema) # Stub unloaded lazy features so they appear as Swagger sections. from litellm.proxy._lazy_features import inject_lazy_stubs, loaded_lazy_modules openapi_schema = inject_lazy_stubs(openapi_schema, loaded_lazy_modules(app)) openapi_schema = ensure_unique_openapi_operation_ids(openapi_schema) # Fix Swagger UI execute path error when server_root_path is set if server_root_path: openapi_schema["servers"] = [{"url": "/" + server_root_path.strip("/")}] app.openapi_schema = openapi_schema return app.openapi_schema if os.getenv("DOCS_FILTERED", "False") == "True" and premium_user: app.openapi = custom_openapi else: # For regular users, use get_openapi_schema to include LLM API schemas app.openapi = get_openapi_schema class UserAPIKeyCacheTTLEnum(enum.Enum): in_memory_cache_ttl = 60 # 1 min ttl ## configure via `general_settings::user_api_key_cache_ttl: ` @app.exception_handler(ProxyException) async def openai_exception_handler(request: Request, exc: ProxyException): # NOTE: DO NOT MODIFY THIS, its crucial to map to Openai exceptions _log_model_access_denial(exc) headers: Final = exc.headers error_dict: Final = with_call_id( JSON_OBJECT.validate_python(exc.to_dict()), error_body_call_id(general_settings_view(), headers.get(LITELLM_CALL_ID_HEADER)), ) status_code: Final = int(exc.code) if exc.code else status.HTTP_500_INTERNAL_SERVER_ERROR _close_dangling_otel_server_span(request, status_code, exc=exc) return JSONResponse( status_code=status_code, content={"error": error_dict}, headers=headers, ) def _log_model_access_denial(exc: ProxyException) -> None: if not isinstance(exc, ModelAccessDeniedProxyException): return verbose_proxy_logger.warning(exc.sanitized_internal_message()) def _close_dangling_otel_server_span(request: Request, status_code: int, exc: Exception | None = None) -> None: parent_otel_span: Final[_Span | None] = getattr(request.state, "parent_otel_span", None) if parent_otel_span is None: return if open_telemetry_logger is None: return # Under OTel V2 the FastAPI instrumentor owns the server span (parent_otel_span # is that same span) and ends it itself with the http.* attributes stamped on # completion. The instrumentor only records an error when the exception reaches # it uncaught, but these handlers swallow it into a JSONResponse, so it never # does; stamp the error.* attributes here (without ending or re-statusing the # span, which the instrumentor still owns) so pre-call failures carry the error # like v1 did. Otherwise close and annotate the dangling span ourselves. try: from litellm.integrations.otel.model.config import is_otel_v2_enabled v2_enabled = is_otel_v2_enabled() except Exception: v2_enabled = False try: from opentelemetry.trace import Status, StatusCode if v2_enabled: if status_code >= 400: open_telemetry_logger.record_error_attributes_on_span(parent_otel_span, exc, status_code) return open_telemetry_logger.set_response_status_code_attribute(parent_otel_span, status_code) if status_code >= 400: open_telemetry_logger.record_error_attributes_on_span(parent_otel_span, exc, status_code) parent_otel_span.set_status(Status(StatusCode.ERROR if status_code >= 400 else StatusCode.OK)) parent_otel_span.end() except Exception as e: verbose_proxy_logger.debug("Error closing dangling OTEL SERVER span: %s", str(e)) finally: if not v2_enabled: request.state.parent_otel_span = None @app.exception_handler(ManagementProblem) async def management_problem_exception_handler(request: Request, exc: ManagementProblem): _close_dangling_otel_server_span(request, exc.problem.status, exc=exc) return problem_response(exc.problem) class _ConfigParamRow(Protocol): param_name: str param_value: Mapping[str, JsonValue] | None class _ConfigOverridesRow(Protocol): config_value: Mapping[str, JsonValue] | None class _SSOConfigRow(Protocol): sso_settings: MutableMapping[str, object] class _InvitationLinkRow(Protocol): user_id: str expires_at: datetime is_accepted: bool accepted_at: datetime | None created_by: str class _UserTableRow(Protocol): user_id: str user_email: str | None user_role: str | None class _UserTeamsRow(Protocol): @property def teams(self) -> Sequence[str]: ... _ProxyModelRow: TypeAlias = "prisma_models.LiteLLM_ProxyModelTable" def _config_param_table(client: PrismaClient | None) -> TableActions[_ConfigParamRow]: return cast( # cast-ok: this is prisma's LiteLLM_Config actions object, which parses its Json column to a mapping "TableActions[_ConfigParamRow]", ConfigRepository(client).table ) class _TTFTRow(TypedDict): api_base: str model: str time_to_first_token: float request_id: str day: str class _LatencyRow(TypedDict): api_base: str | None model: str day: str avg_latency_per_token: float class _ExceptionRow(TypedDict, total=False): combined_model_api_base: str total_exceptions: int exception_counts: Mapping[str, int] @app.exception_handler(RequestValidationError) async def otel_request_validation_exception_handler(request: Request, exc: RequestValidationError): if request.url.path.startswith(MANAGEMENT_V1_PREFIX): validation_errors: Final[Sequence[ValidationErrorDetail]] = exc.errors() problem: Final = request_validation_problem(validation_errors) _close_dangling_otel_server_span(request, problem.status, exc=exc) return problem_response(problem) _close_dangling_otel_server_span(request, 422, exc=exc) return JSONResponse( status_code=422, content={"detail": jsonable_encoder(exc.errors())}, ) @app.exception_handler(Exception) async def otel_unhandled_exception_handler(request: Request, exc: Exception): if isinstance(exc, (ProxyException, HTTPException, RequestValidationError)): raise exc if PrismaDBExceptionHandler.is_database_service_unavailable_error_in_chain(exc): verbose_proxy_logger.warning("Database unavailable during request: %s", type(exc).__name__) return await openai_exception_handler( request=request, exc=PrismaDBExceptionHandler.service_unavailable_proxy_exception(exc) ) verbose_proxy_logger.exception("Unhandled exception in request: %s", type(exc).__name__) if should_report_bug(exc): verbose_proxy_logger.error( bug_report_notice( build_proxy_bug_report( exc, call_type=allowlisted(request.url.path, KNOWN_PROXY_ROUTES), ) ) ) _close_dangling_otel_server_span(request, 500, exc=exc) return JSONResponse( status_code=500, content={ "error": { "message": "Internal server error", "type": "internal_server_error", } }, ) router: Final = APIRouter() def _get_cors_config( cors_origins_env: str | None = None, cors_credentials_env: str | None = None, ): """ Compute CORS allowed origins and credentials flag from environment variables. Extracted into a function so it can be unit-tested without reloading the module. Args: cors_origins_env: Value of LITELLM_CORS_ORIGINS (defaults to os.getenv). cors_credentials_env: Value of LITELLM_CORS_ALLOW_CREDENTIALS (defaults to os.getenv). Returns: Tuple[List[str], bool]: (origins, allow_credentials) """ _origins_raw: Final = cors_origins_env if cors_origins_env is not None else os.getenv("LITELLM_CORS_ORIGINS") if _origins_raw is None or _origins_raw.strip() == "": computed_origins = ["*"] else: computed_origins = [o.strip() for o in _origins_raw.split(",") if o.strip()] # Disable credentials by default when wildcard origins are used — combining # allow_origins=["*"] with allow_credentials=True causes Starlette to reflect # the incoming Origin header, allowing any site to make credentialed requests. # Set LITELLM_CORS_ALLOW_CREDENTIALS=true to explicitly restore the old behaviour # (e.g. for non-browser clients that relied on the Access-Control-Allow-Credentials # header being present regardless of origin). _credentials_raw: Final = ( cors_credentials_env if cors_credentials_env is not None else os.getenv("LITELLM_CORS_ALLOW_CREDENTIALS") ) if _credentials_raw is not None: computed_credentials = _credentials_raw.strip().lower() == "true" else: computed_credentials = "*" not in computed_origins return computed_origins, computed_credentials origins, allow_cors_credentials = _get_cors_config() # get current directory try: current_dir = os.path.dirname(os.path.abspath(__file__)) packaged_ui_path: Final = os.path.join(current_dir, "_experimental", "out") ui_path = packaged_ui_path litellm_asset_prefix: Final = "/litellm-asset-prefix" def _dir_has_content(path: str) -> bool: try: return os.path.isdir(path) and any(os.scandir(path)) except FileNotFoundError: return False def _validate_ui_directory(ui_path: str) -> bool: """ Verify UI directory has minimum required structure. Checks for: - Directory exists - Has index.html (main entry point) - Has _next directory (Next.js assets) Returns True if UI directory appears valid and servable. """ if not os.path.isdir(ui_path): return False # Must have main index.html if not os.path.exists(os.path.join(ui_path, "index.html")): return False # Must have _next directory with Next.js assets next_dir: Final = os.path.join(ui_path, "_next") if not os.path.isdir(next_dir): return False return True def _is_ui_pre_restructured(ui_dir: str) -> bool: """ Detect if UI directory is already pre-restructured and ready to serve. Returns True if: 1. Marker file .litellm_ui_ready exists (created by Dockerfile), OR 2. Restructuring pattern detected (subdirectories with index.html inside) This allows skipping copy/restructure operations on read-only filesystems. """ if not os.path.isdir(ui_dir): return False # Primary signal: marker file created by Dockerfile marker_file: Final = os.path.join(ui_dir, ".litellm_ui_ready") if os.path.exists(marker_file): verbose_proxy_logger.debug("Found UI ready marker: %s", marker_file) return True # Fallback signal: Detect restructuring pattern # After restructuring, routes exist as directories with index.html inside # (e.g., login/index.html instead of login.html) # Check for main index.html first (basic UI structure requirement) if not os.path.exists(os.path.join(ui_dir, "index.html")): return False # Look for ANY subdirectory with index.html (proves restructuring happened) # Ignore directories starting with _ (Next.js internals like _next) try: for entry in os.scandir(ui_dir): if entry.is_dir() and not entry.name.startswith("_"): index_path = os.path.join(entry.path, "index.html") if os.path.exists(index_path): # Found at least one restructured route - this proves the pattern verbose_proxy_logger.debug( "Detected restructured UI via pattern: found %s/index.html", entry.name ) return True except (PermissionError, OSError) as e: verbose_proxy_logger.debug("Could not scan %s for restructuring detection: %s", ui_dir, e) return False # No restructured routes found return False def _try_populate_ui_directory(source_path: str, target_path: str) -> tuple[bool, str]: """ Attempt to populate target UI directory from source. Returns: (success: bool, error_message: str) """ try: os.makedirs(target_path, exist_ok=True) if not _dir_has_content(target_path) and _dir_has_content(source_path): shutil.copytree( source_path, target_path, dirs_exist_ok=True, ) verbose_proxy_logger.info("Successfully populated UI at %s", target_path) return True, "" else: return False, "Source or target directory state invalid" except (PermissionError, OSError) as e: return False, str(e) # Use a writable runtime UI directory whenever possible. # This prevents mutating the packaged UI directory (e.g. site-packages or the repo checkout) # and ensures extensionless routes like /ui/login work via /index.html. is_non_root: Final = os.getenv("LITELLM_NON_ROOT", "").lower() == "true" # Determine runtime UI path # Priority: LITELLM_UI_PATH env var > default path based on is_non_root if is_non_root: default_runtime_ui_path = "/var/lib/litellm/ui" else: default_runtime_ui_path = packaged_ui_path runtime_ui_path: Final = os.getenv("LITELLM_UI_PATH", default_runtime_ui_path) # Validate packaged UI before proceeding if not _validate_ui_directory(packaged_ui_path): verbose_proxy_logger.error( "Packaged UI at %s is invalid or incomplete. UI may not function correctly.", packaged_ui_path ) # Decision tree for UI path selection: # 1. If runtime path == packaged path: use packaged UI directly # 2. If runtime UI exists and is pre-restructured: use it # 3. If runtime UI exists but not restructured: use it (will restructure later) # 4. If runtime UI missing: try to populate from packaged UI # 4a. If population succeeds: use runtime UI # 4b. If population fails: fall back to packaged UI should_use_runtime_path: Final = runtime_ui_path != packaged_ui_path if should_use_runtime_path: is_pre_restructured = _is_ui_pre_restructured(runtime_ui_path) has_content: Final = _dir_has_content(runtime_ui_path) # Case 2: Runtime UI exists and is ready if has_content and is_pre_restructured: verbose_proxy_logger.info("Using pre-restructured UI at %s", runtime_ui_path) ui_path = runtime_ui_path # Case 3: Runtime UI exists but needs restructuring elif has_content and not is_pre_restructured: verbose_proxy_logger.warning( "UI at %s has content but is not properly restructured. Will attempt to restructure in place.", runtime_ui_path, ) ui_path = runtime_ui_path # Case 4: Runtime UI missing - try to populate else: verbose_proxy_logger.info("UI not found at %s. Attempting to populate from packaged UI.", runtime_ui_path) success, error = _try_populate_ui_directory(packaged_ui_path, runtime_ui_path) if success: # Case 4a: Population succeeded ui_path = runtime_ui_path else: # Case 4b: Population failed - fall back to packaged UI verbose_proxy_logger.warning( "Failed to populate UI at %s: %s. Falling back to packaged UI at %s. For read-only deployments, pre-build UI in Dockerfile or set LITELLM_UI_PATH to a writable emptyDir volume.", runtime_ui_path, error, packaged_ui_path, ) ui_path = packaged_ui_path else: # Case 1: Using packaged UI directly (local development) verbose_proxy_logger.info("Using packaged UI directory: %s", packaged_ui_path) ui_path = packaged_ui_path # Validate final UI path if not _validate_ui_directory(ui_path): verbose_proxy_logger.error("Selected UI path %s is invalid or incomplete. UI may not work correctly.", ui_path) # Only modify files if a custom server root path is set AND filesystem is writable if server_root_path and server_root_path != "/": # Check if UI path is writable is_writable = os.access(ui_path, os.W_OK) if not is_writable: verbose_proxy_logger.warning( "Cannot apply server_root_path replacements to UI at %s: path is not writable. Ensure server_root_path is '/' or pre-process UI files in Dockerfile with custom server_root_path.", ui_path, ) else: # Iterate through files in the UI directory for root, dirs, files in os.walk(ui_path): for filename in files: file_path = os.path.join(root, filename) # Skip binary files and files that don't need path replacement if filename.endswith( ( ".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot", ) ): continue try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() # Replace the asset prefix with the server root path modified_content = content.replace( f"{litellm_asset_prefix}", f"{server_root_path}", ) # Replace the /.well-known/litellm-ui-config with the server root path modified_content = modified_content.replace( "/litellm/.well-known/litellm-ui-config", f"{server_root_path}/.well-known/litellm-ui-config", ) with open(file_path, "w", encoding="utf-8") as f: f.write(modified_content) except (UnicodeDecodeError, PermissionError, OSError): # Skip binary files or files we can't write to continue # # Mount the _next directory at the root level app.mount( "/_next", StaticFiles(directory=os.path.join(ui_path, "_next")), name="next_static", ) app.mount( f"{litellm_asset_prefix}/_next", StaticFiles(directory=os.path.join(ui_path, "_next")), name="next_static", ) # print(f"mounted _next at {server_root_path}/ui/_next") app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui") def _restructure_ui_html_files(ui_root: str) -> None: """Ensure each exported HTML route is available as /index.html.""" for current_root, _, files in os.walk(ui_root): rel_root = os.path.relpath(current_root, ui_root) first_segment = "" if rel_root == "." else rel_root.split(os.sep)[0] # Ignore Next.js asset directories if first_segment in {"_next", "litellm-asset-prefix"}: continue for filename in files: if not filename.endswith(".html") or filename == "index.html": continue file_path = os.path.join(current_root, filename) target_dir = os.path.splitext(file_path)[0] target_path = os.path.join(target_dir, "index.html") os.makedirs(target_dir, exist_ok=True) try: os.replace(file_path, target_path) except FileNotFoundError: # Another process may have already moved this file. continue # Handle HTML file restructuring # Only restructure if: # 1. UI is not already pre-restructured # 2. Filesystem is writable try: is_pre_restructured = _is_ui_pre_restructured(ui_path) is_writable = os.access(ui_path, os.W_OK) if is_pre_restructured: verbose_proxy_logger.info("Skipping UI restructuring: %s is already pre-restructured", ui_path) elif not is_writable: verbose_proxy_logger.warning( "Cannot restructure UI at %s: path is not writable. UI may not work correctly for extensionless routes. Pre-build and restructure UI in Dockerfile for read-only deployments.", ui_path, ) else: _restructure_ui_html_files(ui_path) verbose_proxy_logger.info("Restructured UI directory: %s", ui_path) except PermissionError as e: verbose_proxy_logger.exception("Permission error while restructuring UI directory %s: %s", ui_path, e) except Exception as e: verbose_proxy_logger.exception("Error while restructuring UI directory %s: %s", ui_path, e) except Exception: pass current_dir = os.path.dirname(os.path.abspath(__file__)) # ui_path = os.path.join(current_dir, "_experimental", "out") # # Mount this test directory instead # app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui") app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=allow_cors_credentials, allow_methods=["*"], allow_headers=["*"], expose_headers=LITELLM_UI_ALLOW_HEADERS, ) app.add_middleware(PrometheusAuthMiddleware) # Added before InFlightRequestsMiddleware so it nests *inside* it: Starlette # makes the last-added middleware outermost. The billable count is recorded # after the inner app returns, so if this sat outside the in-flight tracker a # request could be counted as drained while its record() had not yet run, and # proxy_shutdown_event could flush and stop the exporter underneath it. app.add_middleware( BillableRequestMetricsMiddleware, # Factory, not an instance: the recorder is resolved on the first request so # it sees premium_user and the billing env vars AFTER proxy_startup_event has # loaded the YAML config's environment_variables. Building it here at import # time would permanently capture recorder=None for YAML-configured # deployments. The lambda reads the module globals at call time. recorder_factory=lambda: ( build_billing_metrics_recorder( premium=premium_user, # Read from the license check, not the premium_user_data module # global: that global is bound once at import and goes stale when # the license arrives via the YAML config's environment_variables. license_data=_license_check.airgapped_license_data, litellm_version=version, ) if build_billing_metrics_recorder is not None else None ), # Unlike the billing recorder this is not license-gated: the admin UI must # report SGR on any deployment. Gated only on a database being configured, # since without one the fold would never be drained. Read at call time, so # it sees prisma_client as of the first request rather than import time. sink_factory=lambda: gateway_request_accumulator if prisma_client is not None else None, ) app.add_middleware(BudgetReservationReleaseMiddleware, release=release_unbound_budget_reservation) app.add_middleware(InFlightRequestsMiddleware) app.add_middleware(SecurityHeadersMiddleware) def mount_swagger_ui(): swagger_directory: Final = os.path.join(current_dir, "swagger") swagger_path = "/" if server_root_path is None else server_root_path if not swagger_path.endswith("/"): swagger_path = swagger_path + "/" custom_root_path_swagger_path: Final = swagger_path + "swagger" app.mount("/swagger", StaticFiles(directory=swagger_directory), name="swagger") # On dropdown expand: one-time fetch to the prefix (triggers lazy load), # then spec re-download so real routes replace the stub. Raw JS (no #