mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test: drop the cwd-relative sys.path.insert calls from the test suite
TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.
Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.
Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.
* test: drop the duplicate imports the sys.path sweep exposed to F811
* test(pre-call-utils): restore the os import the new bedrock tests need
397 lines
14 KiB
Python
397 lines
14 KiB
Python
# What is this?
|
|
## Tests if proxy/auth/auth_utils.py works as expected
|
|
|
|
import sys, os, asyncio, time, random, uuid
|
|
import traceback
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
import pytest
|
|
import litellm
|
|
from litellm.proxy.auth.auth_utils import (
|
|
_allow_model_level_clientside_configurable_parameters,
|
|
)
|
|
from litellm.router import Router
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"allowed_param, input_value, should_return_true",
|
|
[
|
|
("api_base", {"api_base": "http://dummy.com"}, True),
|
|
(
|
|
{"api_base": "https://api.openai.com/v1"},
|
|
{"api_base": "https://api.openai.com/v1"},
|
|
True,
|
|
), # should return True
|
|
(
|
|
{"api_base": "https://api.openai.com/v1"},
|
|
{"api_base": "https://api.anthropic.com/v1"},
|
|
False,
|
|
), # should return False
|
|
(
|
|
{"api_base": "^https://litellm.*direct\.fireworks\.ai/v1$"},
|
|
{"api_base": "https://litellm-dev.direct.fireworks.ai/v1"},
|
|
True,
|
|
),
|
|
(
|
|
{"api_base": "^https://litellm.*novice\.fireworks\.ai/v1$"},
|
|
{"api_base": "https://litellm-dev.direct.fireworks.ai/v1"},
|
|
False,
|
|
),
|
|
],
|
|
)
|
|
def test_configurable_clientside_parameters(
|
|
allowed_param, input_value, should_return_true
|
|
):
|
|
router = Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "dummy-model",
|
|
"litellm_params": {
|
|
"model": "gpt-3.5-turbo",
|
|
"api_key": "dummy-key",
|
|
"configurable_clientside_auth_params": [allowed_param],
|
|
},
|
|
}
|
|
]
|
|
)
|
|
resp = _allow_model_level_clientside_configurable_parameters(
|
|
model="dummy-model",
|
|
param="api_base",
|
|
request_body_value=input_value["api_base"],
|
|
llm_router=router,
|
|
)
|
|
print(resp)
|
|
assert resp == should_return_true
|
|
|
|
|
|
def test_get_end_user_id_from_request_body_always_returns_str():
|
|
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
|
|
from fastapi import Request
|
|
from unittest.mock import MagicMock
|
|
|
|
# Create a mock Request object
|
|
mock_request = MagicMock(spec=Request)
|
|
mock_request.headers = {}
|
|
|
|
request_body = {"user": 123}
|
|
end_user_id = get_end_user_id_from_request_body(
|
|
request_body, dict(mock_request.headers)
|
|
)
|
|
assert end_user_id == "123"
|
|
assert isinstance(end_user_id, str)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"headers, general_settings_config, request_body, expected_user_id",
|
|
[
|
|
# Test 1: user_header_name configured and header present
|
|
(
|
|
{"X-User-ID": "header-user-123"},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"user": "body-user-456"},
|
|
"header-user-123", # Header should take precedence
|
|
),
|
|
# Test 2: user_header_name configured but header not present, fallback to body
|
|
(
|
|
{},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"user": "body-user-456"},
|
|
"body-user-456", # Should fall back to body
|
|
),
|
|
# Test 3: user_header_name not configured, should use body
|
|
(
|
|
{"X-User-ID": "header-user-123"},
|
|
{},
|
|
{"user": "body-user-456"},
|
|
"body-user-456", # Should ignore header when not configured
|
|
),
|
|
# Test 4: user_header_name configured, header present, but no body user
|
|
(
|
|
{"X-Custom-User": "header-only-user"},
|
|
{"user_header_name": "X-Custom-User"},
|
|
{"model": "gpt-4"},
|
|
"header-only-user", # Should use header
|
|
),
|
|
# Test 5: user_header_name configured but header is empty string
|
|
(
|
|
{"X-User-ID": ""},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"user": "body-user-456"},
|
|
"body-user-456", # Should fall back to body when header is empty
|
|
),
|
|
# Test 6: user_header_name configured with case-insensitive header
|
|
(
|
|
{"x-user-id": "lowercase-header-user"},
|
|
{"user_header_name": "x-user-id"},
|
|
{"user": "body-user-456"},
|
|
"lowercase-header-user",
|
|
),
|
|
# Test 7: user_header_name configured but set to None
|
|
(
|
|
{"X-User-ID": "header-user-123"},
|
|
{"user_header_name": None},
|
|
{"user": "body-user-456"},
|
|
"body-user-456", # Should fall back to body when header name is None
|
|
),
|
|
# Test 8: user_header_name is not a string
|
|
(
|
|
{"X-User-ID": "header-user-123"},
|
|
{"user_header_name": 123},
|
|
{"user": "body-user-456"},
|
|
"body-user-456", # Should fall back to body when header name is not a string
|
|
),
|
|
# Test 9: Multiple fallback sources - litellm_metadata
|
|
(
|
|
{},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"litellm_metadata": {"user": "litellm-user-789"}},
|
|
"litellm-user-789",
|
|
),
|
|
# Test 10: Multiple fallback sources - metadata.user_id
|
|
(
|
|
{},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"metadata": {"user_id": "metadata-user-999"}},
|
|
"metadata-user-999",
|
|
),
|
|
# Test 11: Header takes precedence over all body sources
|
|
(
|
|
{"X-User-ID": "header-priority"},
|
|
{"user_header_name": "X-User-ID"},
|
|
{
|
|
"user": "body-user",
|
|
"litellm_metadata": {"user": "litellm-user"},
|
|
"metadata": {"user_id": "metadata-user"},
|
|
},
|
|
"header-priority",
|
|
),
|
|
# Test 12: user_header_name is matched case-insensitively
|
|
(
|
|
{"x-user-id": "lowercase-header-user"},
|
|
{"user_header_name": "X-User-ID"},
|
|
{"user": "body-user-456"},
|
|
"lowercase-header-user",
|
|
),
|
|
],
|
|
)
|
|
def test_get_end_user_id_from_request_body_with_user_header_name(
|
|
headers, general_settings_config, request_body, expected_user_id
|
|
):
|
|
"""Test that get_end_user_id_from_request_body respects user_header_name property"""
|
|
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
|
|
from fastapi import Request
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Create a mock Request object with headers
|
|
mock_request = MagicMock(spec=Request)
|
|
mock_request.headers = headers
|
|
|
|
# Mock general_settings at the proxy_server module level
|
|
with patch("litellm.proxy.proxy_server.general_settings", general_settings_config):
|
|
end_user_id = get_end_user_id_from_request_body(
|
|
request_body, dict(mock_request.headers)
|
|
)
|
|
assert end_user_id == expected_user_id
|
|
|
|
|
|
def test_get_end_user_id_from_request_body_no_user_found():
|
|
"""Test that function returns None when no user ID is found anywhere"""
|
|
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
|
|
from fastapi import Request
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Create a mock Request object with no relevant headers
|
|
mock_request = MagicMock(spec=Request)
|
|
mock_request.headers = {"X-Other-Header": "some-value"}
|
|
|
|
# Mock general_settings with user_header_name that doesn't match headers
|
|
general_settings_config = {"user_header_name": "X-User-ID"}
|
|
|
|
# Request body with no user identifiers
|
|
request_body = {
|
|
"model": "gpt-4",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
}
|
|
|
|
with patch("litellm.proxy.proxy_server.general_settings", general_settings_config):
|
|
end_user_id = get_end_user_id_from_request_body(
|
|
request_body, dict(mock_request.headers)
|
|
)
|
|
assert end_user_id is None
|
|
|
|
|
|
def test_get_end_user_id_from_request_body_backwards_compatibility():
|
|
"""Test that function works with just request_body parameter (backwards compatibility)"""
|
|
from litellm.proxy.auth.auth_utils import get_end_user_id_from_request_body
|
|
|
|
# Test with just request_body - should work like before
|
|
request_body = {"user": "test-user-123"}
|
|
end_user_id = get_end_user_id_from_request_body(request_body)
|
|
assert end_user_id == "test-user-123"
|
|
|
|
# Test with litellm_metadata
|
|
request_body = {"litellm_metadata": {"user": "litellm-user-456"}}
|
|
end_user_id = get_end_user_id_from_request_body(request_body)
|
|
assert end_user_id == "litellm-user-456"
|
|
|
|
# Test with metadata.user_id
|
|
request_body = {"metadata": {"user_id": "metadata-user-789"}}
|
|
end_user_id = get_end_user_id_from_request_body(request_body)
|
|
assert end_user_id == "metadata-user-789"
|
|
|
|
# Test with no user - should return None
|
|
request_body = {"model": "gpt-4"}
|
|
end_user_id = get_end_user_id_from_request_body(request_body)
|
|
assert end_user_id is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"request_data, expected_model",
|
|
[
|
|
(
|
|
{"target_model_names": "gpt-3.5-turbo, gpt-4o-mini-general-deployment"},
|
|
["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"],
|
|
),
|
|
({"target_model_names": "gpt-3.5-turbo"}, ["gpt-3.5-turbo"]),
|
|
(
|
|
{"model": "gpt-3.5-turbo, gpt-4o-mini-general-deployment"},
|
|
["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"],
|
|
),
|
|
({"model": "gpt-3.5-turbo"}, "gpt-3.5-turbo"),
|
|
],
|
|
)
|
|
def test_get_model_from_request(request_data, expected_model):
|
|
from litellm.proxy.auth.auth_utils import get_model_from_request
|
|
|
|
request_data = {
|
|
"target_model_names": "gpt-3.5-turbo, gpt-4o-mini-general-deployment"
|
|
}
|
|
route = "/openai/deployments/gpt-3.5-turbo"
|
|
model = get_model_from_request(request_data, "/v1/files")
|
|
assert model == ["gpt-3.5-turbo", "gpt-4o-mini-general-deployment"]
|
|
|
|
|
|
def test_get_customer_user_header_from_mapping_returns_customer_header():
|
|
from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping
|
|
|
|
mappings = [
|
|
{"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"},
|
|
{"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"},
|
|
]
|
|
result = get_customer_user_header_from_mapping(mappings)
|
|
assert result == ["x-openwebui-user-email"]
|
|
|
|
|
|
def test_get_customer_user_header_from_mapping_no_customer_returns_none():
|
|
from litellm.proxy.auth.auth_utils import get_customer_user_header_from_mapping
|
|
|
|
mappings = [
|
|
{"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"}
|
|
]
|
|
result = get_customer_user_header_from_mapping(mappings)
|
|
assert result is None
|
|
|
|
# Also support a single mapping dict
|
|
single_mapping = {
|
|
"header_name": "X-Only-Internal",
|
|
"litellm_user_role": "internal_user",
|
|
}
|
|
result = get_customer_user_header_from_mapping(single_mapping)
|
|
assert result is None
|
|
|
|
|
|
def test_get_internal_user_header_from_mapping_returns_internal_header():
|
|
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
|
|
|
|
mappings = [
|
|
{"header_name": "X-OpenWebUI-User-Id", "litellm_user_role": "internal_user"},
|
|
{"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"},
|
|
]
|
|
|
|
result = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(mappings)
|
|
assert result == "X-OpenWebUI-User-Id"
|
|
|
|
|
|
def test_get_internal_user_header_from_mapping_no_internal_returns_none():
|
|
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
|
|
|
|
mappings = [
|
|
{"header_name": "X-OpenWebUI-User-Email", "litellm_user_role": "customer"}
|
|
]
|
|
result = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(mappings)
|
|
assert result is None
|
|
|
|
# Also support single mapping dict
|
|
single_mapping = {"header_name": "X-Only-Customer", "litellm_user_role": "customer"}
|
|
result = LiteLLMProxyRequestSetup.get_internal_user_header_from_mapping(
|
|
single_mapping
|
|
)
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"request_data, route, expected_model",
|
|
[
|
|
# Vertex AI passthrough URL patterns
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/us-central1/publishers/google/models/gemini-1.5-pro:generateContent",
|
|
"gemini-1.5-pro",
|
|
),
|
|
(
|
|
{},
|
|
"/vertex_ai/v1beta1/projects/my-project/locations/us-central1/publishers/google/models/gemini-1.0-pro:streamGenerateContent",
|
|
"gemini-1.0-pro",
|
|
),
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/asia-southeast1/publishers/google/models/gemini-2.0-flash:generateContent",
|
|
"gemini-2.0-flash",
|
|
),
|
|
# Model without method suffix (no colon) - should still extract
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/us-central1/publishers/google/models/gemini-pro",
|
|
"gemini-pro", # Should match even without colon
|
|
),
|
|
# Request body model takes precedence over URL
|
|
(
|
|
{"model": "gpt-4o"},
|
|
"/vertex_ai/v1/projects/my-project/locations/us-central1/publishers/google/models/gemini-1.5-pro:generateContent",
|
|
"gpt-4o",
|
|
),
|
|
# Non-vertex route should not extract from vertex pattern
|
|
({}, "/openai/v1/chat/completions", None),
|
|
# Azure deployment pattern should still work
|
|
({}, "/openai/deployments/my-deployment/chat/completions", "my-deployment"),
|
|
# Custom model_name with slashes (e.g., gcp/google/gemini-2.5-flash)
|
|
# This is the NVIDIA P0 bug fix - regex should capture full model name including slashes
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/us-central1/publishers/google/models/gcp/google/gemini-2.5-flash:generateContent",
|
|
"gcp/google/gemini-2.5-flash",
|
|
),
|
|
# Another custom model_name with slashes
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/global/publishers/google/models/gcp/google/gemini-3-flash-preview:generateContent",
|
|
"gcp/google/gemini-3-flash-preview",
|
|
),
|
|
# Model name with single slash
|
|
(
|
|
{},
|
|
"/vertex_ai/v1/projects/my-project/locations/us-central1/publishers/google/models/custom/model:generateContent",
|
|
"custom/model",
|
|
),
|
|
],
|
|
)
|
|
def test_get_model_from_request_vertex_ai_passthrough(
|
|
request_data, route, expected_model
|
|
):
|
|
"""Test that get_model_from_request correctly extracts Vertex AI model from URL"""
|
|
from litellm.proxy.auth.auth_utils import get_model_from_request
|
|
|
|
model = get_model_from_request(request_data, route)
|
|
assert model == expected_model
|