This commit is contained in:
Michał Cichoń 2026-09-23 14:47:10 +00:00 • committed by GitHub
commit 9cfa41680f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 103 additions and 3 deletions

View file

@ -104,6 +104,10 @@ response = completion(model="openai/gpt-4o", messages=[{"role": "user", "content
response = completion(model="anthropic/claude-sonnet-4-20250514", messages=[{"role": "user", "content": "Hello!"}])
```
By default, LiteLLM loads `.env` when imported in DEV mode. To disable this for the SDK and proxy, set
`LITELLM_DISABLE_DOTENV=1` in the process environment before importing LiteLLM. Setting it inside `.env` is too late
because LiteLLM reads the flag before loading that file
### AI Gateway (Proxy Server)
[**Getting Started - E2E Tutorial**](https://docs.litellm.ai/docs/proxy/docker_quick_start) - Setup virtual keys, make your first request

View file

@ -11,12 +11,14 @@ warnings.filterwarnings("ignore", message=".*Accessing the.*attribute on the ins
# cannot enforce it at runtime, which floods proxy boot once such a type is schema-walked
warnings.filterwarnings("ignore", message=".*`ReadOnly` qualifier.*")
### INIT VARIABLES #########################
import threading
import os
import threading
# Load .env before any other litellm imports so env vars (e.g. LITELLM_UI_SESSION_DURATION) are available
import dotenv as _dotenv
from ._dotenv_loader import should_load_dotenv as _should_load_dotenv
def _dev_env_hot_reload_enabled() -> bool:
"""The proxy exports this flag when started with ``--reload``. A reloaded
@ -26,7 +28,7 @@ def _dev_env_hot_reload_enabled() -> bool:
return os.getenv("LITELLM_DEV_ENV_HOT_RELOAD") == "True"
if os.getenv("LITELLM_MODE", "DEV") == "DEV":
if _should_load_dotenv():
_dotenv.load_dotenv(override=_dev_env_hot_reload_enabled())
from collections.abc import Mapping, Sequence

View file

@ -0,0 +1,9 @@
import os
from typing import Final
_TRUTHY_VALUES: Final = frozenset({"1", "true", "t", "yes", "y"})
def should_load_dotenv() -> bool:
disable_dotenv: Final = os.getenv("LITELLM_DISABLE_DOTENV", "").strip().casefold()
return os.getenv("LITELLM_MODE", "DEV") == "DEV" and disable_dotenv not in _TRUTHY_VALUES

View file

@ -18,6 +18,7 @@ from dotenv import load_dotenv
from pydantic import BaseModel, ConfigDict
import litellm
from litellm._dotenv_loader import should_load_dotenv
from litellm.constants import DEFAULT_NUM_WORKERS_LITELLM_PROXY
from litellm.proxy.db.pgbouncer import (
PgBouncerError,
@ -52,7 +53,7 @@ sys.path.append(os.getcwd())
config_filename: Final = "litellm.secrets"
litellm_mode: Final = os.getenv("LITELLM_MODE", "DEV") # "PRODUCTION", "DEV"
if litellm_mode == "DEV":
if should_load_dotenv():
load_dotenv()
from enum import Enum

View file

@ -0,0 +1,84 @@
import os
import subprocess
import sys
import textwrap
from collections.abc import Mapping
from pathlib import Path
from typing import Final
import pytest
_CONTROL_ENV_VARIABLES: Final = frozenset(
{
"LITELLM_DISABLE_DOTENV",
"LITELLM_DEV_ENV_HOT_RELOAD",
"LITELLM_DOTENV_LOADED",
"LITELLM_MODE",
}
)
_REPOSITORY_ROOT: Final = Path(__file__).resolve().parents[2]
def _dotenv_was_loaded(module: str, environment: Mapping[str, str]) -> bool:
process_environment: Final = {
key: value for key, value in os.environ.items() if key not in _CONTROL_ENV_VARIABLES
} | dict(environment)
script: Final = textwrap.dedent(
f"""
import os
import dotenv
def load_dotenv(*args, **kwargs):
os.environ["LITELLM_DOTENV_LOADED"] = "1"
return True
dotenv.load_dotenv = load_dotenv
import {module}
print(f"LITELLM_DOTENV_LOADED={{os.environ.get('LITELLM_DOTENV_LOADED', '0')}}")
"""
)
result: Final = subprocess.run(
[sys.executable, "-c", script],
cwd=_REPOSITORY_ROOT,
env=process_environment,
capture_output=True,
text=True,
check=True,
)
marker: Final = "LITELLM_DOTENV_LOADED="
output_line: Final = next(line for line in result.stdout.splitlines() if line.startswith(marker))
return output_line.removeprefix(marker) == "1"
def test_default_dev_mode_loads_dotenv() -> None:
assert _dotenv_was_loaded("litellm", {})
@pytest.mark.parametrize("value", ("1", "true", "t", "yes", "y", "TRUE", "YeS"))
def test_disable_dotenv_truthy_values_prevent_loading(value: str) -> None:
assert not _dotenv_was_loaded("litellm", {"LITELLM_DISABLE_DOTENV": value})
def test_disable_dotenv_false_value_preserves_loading() -> None:
assert _dotenv_was_loaded("litellm", {"LITELLM_DISABLE_DOTENV": "false"})
def test_production_mode_still_skips_dotenv() -> None:
assert not _dotenv_was_loaded("litellm", {"LITELLM_MODE": "PRODUCTION"})
def test_disable_dotenv_wins_over_dev_hot_reload() -> None:
environment: Final = {
"LITELLM_DISABLE_DOTENV": "1",
"LITELLM_DEV_ENV_HOT_RELOAD": "True",
}
assert not _dotenv_was_loaded("litellm", environment)
def test_proxy_cli_respects_disable_dotenv() -> None:
assert not _dotenv_was_loaded("litellm.proxy.proxy_cli", {"LITELLM_DISABLE_DOTENV": "1"})
def test_proxy_cli_default_dev_mode_loads_dotenv() -> None:
assert _dotenv_was_loaded("litellm.proxy.proxy_cli", {})