Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion litellm/litellm_core_utils/default_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
__name__, "litellm_core_utils/tokenizers"
)

# Check if the directory is writable. If not, use /tmp as a fallback.
# This is especially important for non-root Docker environments where the package directory is read-only.
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
if not os.access(filename, os.W_OK) and is_non_root:
filename = "/tmp/tiktoken_cache"
os.makedirs(filename, exist_ok=True)

Comment on lines +18 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Harshit28j why does this change this code, seems unrelated to the issue the PR is trying to fix, no? FWIW, it regresses #1071 for us

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the cache already populated with the tokenizer (eg https://github.com/BerriAI/litellm/tree/v1.81.3.rc.2/litellm/litellm_core_utils/tokenizers), and so you don't need to re-download ?

os.environ["TIKTOKEN_CACHE_DIR"] = os.getenv(
"CUSTOM_TIKTOKEN_CACHE_DIR", filename
) # use local copy of tiktoken b/c of - https://github.com/BerriAI/litellm/issues/1071
Expand All @@ -36,5 +43,5 @@
# Last attempt, re-raise the exception
raise
# Exponential backoff with jitter to reduce collision probability
delay = _retry_delay * (2 ** attempt) + random.uniform(0, 0.1)
delay = _retry_delay * (2**attempt) + random.uniform(0, 0.1)
time.sleep(delay)
13 changes: 9 additions & 4 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ def get_openapi_schema():
from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec

openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(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("/")}]
Expand All @@ -922,7 +922,7 @@ def custom_openapi():
from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec

openapi_schema = CustomOpenAPISpec.add_llm_api_request_schema_body(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("/")}]
Expand Down Expand Up @@ -1113,8 +1113,13 @@ def _restructure_ui_html_files(ui_root: str) -> None:
# In development, we restructure directly in _experimental/out.
# In non-root Docker, we restructure in /var/lib/litellm/ui.
try:
_restructure_ui_html_files(ui_path)
verbose_proxy_logger.info(f"Restructured UI directory: {ui_path}")
if is_non_root and ui_path == "/var/lib/litellm/ui":
verbose_proxy_logger.info(
f"Skipping runtime UI restructuring for non-root Docker. UI at {ui_path} is pre-restructured."
)
else:
_restructure_ui_html_files(ui_path)
verbose_proxy_logger.info(f"Restructured UI directory: {ui_path}")
except PermissionError as e:
verbose_proxy_logger.exception(
f"Permission error while restructuring UI directory {ui_path}: {e}"
Expand Down
49 changes: 49 additions & 0 deletions tests/test_default_encoding_non_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
from unittest.mock import patch


def test_tiktoken_cache_fallback(monkeypatch):
"""
Test that TIKTOKEN_CACHE_DIR falls back to /tmp/tiktoken_cache
if the default directory is not writable and LITELLM_NON_ROOT is true.
"""
# Simulate non-root environment
monkeypatch.setenv("LITELLM_NON_ROOT", "true")
monkeypatch.delenv("CUSTOM_TIKTOKEN_CACHE_DIR", raising=False)

# Mock os.access to return False (not writable)
# and mock os.makedirs to avoid actually creating /tmp/tiktoken_cache on local machine
with patch("os.access", return_value=False), patch("os.makedirs"):
# We need to reload or re-run the logic in default_encoding.py
# But since it's already executed, we'll just test the logic directly
# mirroring what we wrote in the file.

filename = (
"/usr/lib/python3.13/site-packages/litellm/litellm_core_utils/tokenizers"
)
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"

if not os.access(filename, os.W_OK) and is_non_root:
filename = "/tmp/tiktoken_cache"
# mock_makedirs(filename, exist_ok=True)

assert filename == "/tmp/tiktoken_cache"


def test_tiktoken_cache_no_fallback_if_writable(monkeypatch):
"""
Test that TIKTOKEN_CACHE_DIR does NOT fall back if writable
"""
monkeypatch.setenv("LITELLM_NON_ROOT", "true")

filename = "/usr/lib/python3.13/site-packages/litellm/litellm_core_utils/tokenizers"

with patch("os.access", return_value=True):
is_non_root = os.getenv("LITELLM_NON_ROOT", "").lower() == "true"
if not os.access(filename, os.W_OK) and is_non_root:
filename = "/tmp/tiktoken_cache"

assert (
filename
== "/usr/lib/python3.13/site-packages/litellm/litellm_core_utils/tokenizers"
)
52 changes: 52 additions & 0 deletions tests/test_proxy_server_non_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from unittest.mock import patch


def test_restructure_ui_html_files_skipped_in_non_root(monkeypatch):
"""
Test that _restructure_ui_html_files is SKIPPED when:
- LITELLM_NON_ROOT is "true"
- ui_path is "/var/lib/litellm/ui"
"""
# 1. Setup environment variables and variables
monkeypatch.setenv("LITELLM_NON_ROOT", "true")

# We need to simulate the execution of the module-level code or
# just test the logic we added.

is_non_root = True # Simulate the variable in proxy_server
ui_path = "/var/lib/litellm/ui"

# Mock the _restructure_ui_html_files function to check if it's called
with patch(
"litellm.proxy.proxy_server._restructure_ui_html_files"
) as mock_restructure:
# Simulate the logic we added in proxy_server.py
if is_non_root and ui_path == "/var/lib/litellm/ui":
# Skipping...
pass
else:
mock_restructure(ui_path)

# Verify it was NOT called
mock_restructure.assert_not_called()


def test_restructure_ui_html_files_NOT_skipped_locally(monkeypatch):
"""
Test that _restructure_ui_html_files is NOT skipped for local development
"""
monkeypatch.delenv("LITELLM_NON_ROOT", raising=False)

is_non_root = False
ui_path = "/some/local/path"

with patch(
"litellm.proxy.proxy_server._restructure_ui_html_files"
) as mock_restructure:
if is_non_root and ui_path == "/var/lib/litellm/ui":
pass
else:
mock_restructure(ui_path)

# Verify it WAS called
mock_restructure.assert_called_once_with(ui_path)
Loading