Merge branch 'doc_yj_feb21' of github.com:BerriAI/litellm into litellm_yj_docs_feb21

This commit is contained in:
yuneng-jiang
2026-02-21 17:58:44 -08:00
10 changed files with 245 additions and 53 deletions
+5
View File
@@ -676,6 +676,11 @@ class LiteLLMRoutes(enum.Enum):
)
# Pre-computed tuple for fast startswith() checks against mapped pass-through routes.
# Defined once here and imported by auth/route_checks modules.
MAPPED_PASS_THROUGH_PREFIXES = tuple(LiteLLMRoutes.mapped_pass_through_routes.value)
class LiteLLMPromptInjectionParams(LiteLLMPydanticObjectBase):
heuristics_check: bool = False
vector_db_check: bool = False
+3 -3
View File
@@ -5,6 +5,7 @@ from fastapi import HTTPException, Request, status
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import (
MAPPED_PASS_THROUGH_PREFIXES,
CommonProxyErrors,
LiteLLM_UserTable,
LiteLLMRoutes,
@@ -346,9 +347,8 @@ class RouteChecks:
if RouteChecks._is_azure_openai_route(route=route):
return True
for _llm_passthrough_route in LiteLLMRoutes.mapped_pass_through_routes.value:
if _llm_passthrough_route in route:
return True
if route.startswith(MAPPED_PASS_THROUGH_PREFIXES):
return True
return False
@staticmethod
+24 -35
View File
@@ -78,6 +78,9 @@ except ImportError as e:
user_api_key_service_logger_obj = ServiceLogging() # used for tracking latency on OTEL
# Pre-computed constants to avoid repeated enum attribute access
_PUBLIC_ROUTES = LiteLLMRoutes.public_routes.value
custom_litellm_key_header = APIKeyHeader(
name=SpecialHeaders.custom_litellm_api_key.value,
auto_error=False,
@@ -358,15 +361,6 @@ def get_api_key(
google_auth_key: str = _safe_get_request_query_params(request).get("key") or ""
passed_in_key = google_auth_key
api_key = google_auth_key
elif pass_through_endpoints is not None:
for endpoint in pass_through_endpoints:
if endpoint.get("path", "") == route:
headers: Optional[dict] = endpoint.get("headers", None)
if headers is not None:
header_key: str = headers.get("litellm_user_api_key", "")
if request.headers.get(header_key) is not None:
api_key = request.headers.get(header_key) or ""
passed_in_key = api_key
return api_key, passed_in_key
@@ -376,29 +370,22 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints(
pass_through_endpoints: Optional[List[dict]],
api_key: str,
) -> Union[UserAPIKeyAuth, str]:
is_mapped_pass_through_route: bool = False
for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value: # type: ignore
if route.startswith(mapped_route):
is_mapped_pass_through_route = True
if is_mapped_pass_through_route:
if request.headers.get("litellm_user_api_key") is not None:
api_key = request.headers.get("litellm_user_api_key") or ""
# Fast path: nothing to check
is_mapped = route.startswith(MAPPED_PASS_THROUGH_PREFIXES)
if not is_mapped and pass_through_endpoints is None:
return api_key
if is_mapped:
value = request.headers.get("litellm_user_api_key")
if value is not None:
api_key = value
if pass_through_endpoints is not None:
for endpoint in pass_through_endpoints:
if isinstance(endpoint, dict) and endpoint.get("path", "") == route:
## IF AUTH DISABLED
if endpoint.get("auth") is not True:
return UserAPIKeyAuth()
## IF AUTH ENABLED
### IF CUSTOM PARSER REQUIRED
if (
endpoint.get("custom_auth_parser") is not None
and endpoint.get("custom_auth_parser") == "langfuse"
):
"""
- langfuse returns {'Authorization': 'Basic YW55dGhpbmc6YW55dGhpbmc'}
- check the langfuse public key if it contains the litellm api key
"""
if endpoint.get("custom_auth_parser") == "langfuse":
import base64
api_key = api_key.replace("Basic ", "").strip()
@@ -409,11 +396,10 @@ async def check_api_key_for_custom_headers_or_pass_through_endpoints(
headers = endpoint.get("headers", None)
if headers is not None:
header_key = headers.get("litellm_user_api_key", "")
if (
isinstance(request.headers, dict)
and request.headers.get(key=header_key) is not None # type: ignore
):
api_key = request.headers.get(key=header_key) # type: ignore
value = request.headers.get(header_key)
if value is not None:
api_key = value
break # found matching endpoint, stop looping
return api_key
@@ -426,6 +412,7 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
azure_apim_header: Optional[str],
request_data: dict,
custom_litellm_key_header: Optional[str] = None,
route: Optional[str] = None,
) -> UserAPIKeyAuth:
from litellm.proxy.proxy_server import (
general_settings,
@@ -444,7 +431,8 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
parent_otel_span: Optional[Span] = None
start_time = datetime.now()
route: str = get_request_route(request=request)
if route is None:
route = get_request_route(request=request)
valid_token: Optional[UserAPIKeyAuth] = None
custom_auth_api_key: bool = False
@@ -483,7 +471,7 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
parent_otel_span = (
open_telemetry_logger.create_litellm_proxy_request_started_span(
start_time=start_time,
headers=dict(request.headers),
headers=_safe_get_request_headers(request),
)
)
@@ -515,7 +503,7 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
######## Route Checks Before Reading DB / Cache for "token" ################
if (
route in LiteLLMRoutes.public_routes.value # type: ignore
route in _PUBLIC_ROUTES
or route_in_additonal_public_routes(current_route=route)
):
# check if public endpoint
@@ -1371,6 +1359,7 @@ async def user_api_key_auth(
azure_apim_header=azure_apim_header,
request_data=request_data,
custom_litellm_key_header=custom_litellm_key_header,
route=route,
)
## ENSURE DISABLE ROUTE WORKS ACROSS ALL USER AUTH FLOWS ##
@@ -135,17 +135,29 @@ def _safe_set_request_parsed_body(
def _safe_get_request_headers(request: Optional[Request]) -> dict:
"""
[Non-Blocking] Safely get the request headers
[Non-Blocking] Safely get the request headers.
Caches the result on request.state to avoid re-creating dict(request.headers) per call.
Warning: Callers must NOT mutate the returned dict it is shared across
all callers within the same request via the cache.
"""
if request is None:
return {}
cached = getattr(request.state, "_cached_headers", None)
if cached is not None:
return cached
try:
if request is None:
return {}
return dict(request.headers)
headers = dict(request.headers)
except Exception as e:
verbose_proxy_logger.debug(
"Unexpected error reading request headers - {}".format(e)
)
return {}
headers = {}
try:
request.state._cached_headers = headers
except Exception:
pass # request.state may not be available in all contexts
return headers
def check_file_size_under_limit(
@@ -40,9 +40,9 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.passthrough import BasePassthroughUtils
from litellm.proxy._types import (
MAPPED_PASS_THROUGH_PREFIXES,
ConfigFieldInfo,
ConfigFieldUpdate,
LiteLLMRoutes,
PassThroughEndpointResponse,
PassThroughGenericEndpoint,
ProxyException,
@@ -2058,9 +2058,8 @@ class InitPassThroughEndpointHelpers:
bool: True if route is a registered pass-through endpoint, False otherwise
"""
## CHECK IF MAPPED PASS THROUGH ENDPOINT
for mapped_route in LiteLLMRoutes.mapped_pass_through_routes.value:
if route.startswith(mapped_route):
return True
if route.startswith(MAPPED_PASS_THROUGH_PREFIXES):
return True
# Fast path: check if any registered route key contains this path
# Keys are in format: "{endpoint_id}:exact:{path}:{methods}" or "{endpoint_id}:subpath:{path}:{methods}"
@@ -443,6 +443,166 @@ async def test_return_user_api_key_auth_obj_user_spend_and_budget():
assert result.user_email == "test@example.com"
# ── Regression tests for auth optimizations ──────────────────────────────────
@pytest.mark.asyncio
async def test_check_api_key_normal_route_returns_api_key():
"""Normal route, no pass-through config -> returns api_key unchanged."""
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
request = MagicMock()
request.headers = {}
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/chat/completions",
pass_through_endpoints=None,
api_key="sk-test123",
)
assert result == "sk-test123"
@pytest.mark.asyncio
async def test_check_api_key_mapped_pass_through_with_header():
"""Route /anthropic/v1/messages, header litellm_user_api_key set -> extracts key."""
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
request = MagicMock()
request.headers = {"litellm_user_api_key": "sk-from-header"}
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/anthropic/v1/messages",
pass_through_endpoints=None,
api_key="sk-original",
)
assert result == "sk-from-header"
@pytest.mark.asyncio
async def test_check_api_key_configured_endpoint_auth_disabled():
"""Pass-through endpoint with auth: false -> returns UserAPIKeyAuth()."""
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
request = MagicMock()
request.headers = {}
endpoints = [{"path": "/custom/endpoint", "auth": False}]
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/custom/endpoint",
pass_through_endpoints=endpoints,
api_key="sk-test",
)
assert isinstance(result, UserAPIKeyAuth)
@pytest.mark.asyncio
async def test_check_api_key_configured_endpoint_langfuse_parser():
"""Langfuse endpoint with Base64 auth -> parses correctly."""
import base64
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
public_key = "sk-lf-public"
secret_key = "sk-lf-secret"
basic_auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
request = MagicMock()
request.headers = {}
endpoints = [
{"path": "/langfuse/api", "auth": True, "custom_auth_parser": "langfuse"}
]
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/langfuse/api",
pass_through_endpoints=endpoints,
api_key=f"Basic {basic_auth}",
)
assert result == public_key
@pytest.mark.asyncio
async def test_check_api_key_configured_endpoint_custom_header():
"""Pass-through endpoint with custom header config -> extracts from configured header."""
from litellm.proxy.auth.user_api_key_auth import (
check_api_key_for_custom_headers_or_pass_through_endpoints,
)
request = MagicMock()
request.headers = {"x-custom-key": "sk-custom-value"}
endpoints = [
{
"path": "/custom/endpoint",
"auth": True,
"headers": {"litellm_user_api_key": "x-custom-key"},
}
]
result = await check_api_key_for_custom_headers_or_pass_through_endpoints(
request=request,
route="/custom/endpoint",
pass_through_endpoints=endpoints,
api_key="sk-original",
)
assert result == "sk-custom-value"
def test_get_api_key_without_pass_through_branch():
"""
Regression test for Round 2: after removing the elif pass_through_endpoints branch
from get_api_key, the key should still be extractable via Bearer token even when
pass_through_endpoints is configured (the later check_api_key_for_... call handles it).
"""
endpoints = [
{
"path": "/custom/endpoint",
"auth": True,
"headers": {"litellm_user_api_key": "x-custom-key"},
}
]
request = MagicMock()
request.headers = {"x-custom-key": "sk-custom-value"}
api_key, passed_in_key = get_api_key(
custom_litellm_key_header=None,
api_key="Bearer sk-test-key",
azure_api_key_header=None,
anthropic_api_key_header=None,
google_ai_studio_api_key_header=None,
azure_apim_header=None,
pass_through_endpoints=endpoints,
route="/custom/endpoint",
request=request,
)
# Bearer token should be extracted normally regardless of pass_through_endpoints
assert api_key == "sk-test-key"
assert passed_in_key == "Bearer sk-test-key"
def test_safe_get_request_headers_caching():
"""Call _safe_get_request_headers twice on same request, assert returns same dict object."""
from starlette.datastructures import State
from litellm.proxy.common_utils.http_parsing_utils import (
_safe_get_request_headers,
)
request = MagicMock()
request.headers = {"content-type": "application/json", "authorization": "Bearer sk-123"}
request.state = State() # real State object that supports attribute setting
result1 = _safe_get_request_headers(request)
result2 = _safe_get_request_headers(request)
assert result1 is result2, "Second call should return the same cached dict object"
def test_proxy_admin_jwt_auth_includes_identity_fields():
"""
Test that the proxy admin early-return path in JWT auth populates
@@ -7,6 +7,7 @@ import orjson
import pytest
from fastapi import Request
from fastapi.testclient import TestClient
from starlette.datastructures import State
sys.path.insert(
0, os.path.abspath("../../../..")
@@ -36,6 +37,7 @@ async def test_request_body_caching():
"""
# Create a mock request with a JSON body
mock_request = MagicMock()
mock_request.state = State()
test_data = {"key": "value"}
# Use AsyncMock for the body method
mock_request.body = AsyncMock(return_value=orjson.dumps(test_data))
@@ -69,6 +71,7 @@ async def test_form_data_parsing():
"""
# Create a mock request with form data
mock_request = MagicMock()
mock_request.state = State()
test_data = {"name": "test_user", "message": "hello world"}
# Mock the form method to return the test data as an awaitable
@@ -104,7 +107,8 @@ async def test_form_data_with_json_metadata():
"""
# Create a mock request with form data containing JSON metadata
mock_request = MagicMock()
mock_request.state = State()
# Metadata is sent as a JSON string in form data
metadata_json_string = json.dumps({
"user_id": "12345",
@@ -152,7 +156,8 @@ async def test_form_data_with_invalid_json_metadata():
"""
# Create a mock request with form data containing invalid JSON metadata
mock_request = MagicMock()
mock_request.state = State()
test_data = {
"model": "whisper-1",
"file": "audio.mp3",
@@ -178,7 +183,8 @@ async def test_form_data_without_metadata():
"""
# Create a mock request with form data without metadata
mock_request = MagicMock()
mock_request.state = State()
test_data = {
"model": "whisper-1",
"file": "audio.mp3",
@@ -208,7 +214,8 @@ async def test_form_data_with_empty_metadata():
"""
# Create a mock request with form data containing empty metadata
mock_request = MagicMock()
mock_request.state = State()
test_data = {
"model": "whisper-1",
"file": "audio.mp3",
@@ -240,7 +247,8 @@ async def test_form_data_with_dict_metadata():
"""
# Create a mock request with form data where metadata is already a dict
mock_request = MagicMock()
mock_request.state = State()
metadata_dict = {
"user_id": "12345",
"tags": ["test"]
@@ -275,7 +283,8 @@ async def test_form_data_with_none_metadata():
"""
# Create a mock request with form data where metadata is None
mock_request = MagicMock()
mock_request.state = State()
test_data = {
"model": "whisper-1",
"file": "audio.mp3",
@@ -303,6 +312,7 @@ async def test_empty_request_body():
"""
# Create a mock request with an empty body
mock_request = MagicMock()
mock_request.state = State()
mock_request.body = AsyncMock(return_value=b"") # Empty bytes as an awaitable
mock_request.headers = {"content-type": "application/json"}
mock_request.scope = {}
@@ -327,6 +337,7 @@ async def test_circular_reference_handling():
"""
# Create a mock request with initial data
mock_request = MagicMock()
mock_request.state = State()
initial_body = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
@@ -366,6 +377,7 @@ async def test_json_parsing_error_handling():
"""
# Test case 1: Trailing comma error
mock_request = MagicMock()
mock_request.state = State()
invalid_json_with_trailing_comma = b'''{
"model": "gpt-4o",
"tools": [
@@ -394,6 +406,7 @@ async def test_json_parsing_error_handling():
# Test case 2: Unquoted property name error
mock_request2 = MagicMock()
mock_request2.state = State()
invalid_json_unquoted_property = b'''{
"model": "gpt-4o",
"tools": [
@@ -418,6 +431,7 @@ async def test_json_parsing_error_handling():
# Test case 3: Valid JSON should work normally
mock_request3 = MagicMock()
mock_request3.state = State()
valid_json = b'''{
"model": "gpt-4o",
"tools": [
@@ -749,6 +763,7 @@ async def test_request_body_with_html_script_tags():
}
mock_request = MagicMock()
mock_request.state = State()
mock_request.body = AsyncMock(return_value=orjson.dumps(test_payload))
mock_request.headers = {"content-type": "application/json"}
mock_request.scope = {}
@@ -17,6 +17,7 @@ export interface MemberTableProps {
roleTooltip?: string;
extraColumns?: ColumnsType<Member>;
showDeleteForMember?: (member: Member) => boolean;
emptyText?: string;
}
export default function MemberTable({
@@ -29,6 +30,7 @@ export default function MemberTable({
roleTooltip,
extraColumns = [],
showDeleteForMember,
emptyText,
}: MemberTableProps) {
const baseColumns: ColumnsType<Member> = [
{
@@ -109,6 +111,7 @@ export default function MemberTable({
pagination={false}
size="small"
scroll={{ x: "max-content" }}
locale={emptyText ? { emptyText } : undefined}
/>
{onAddMember && canEdit && (
<Button icon={<UserAddOutlined />} type="primary" onClick={onAddMember}>
@@ -1,5 +1,6 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, test, expect } from "vitest";
import OrganizationInfoView from "./organization_view";
@@ -105,6 +106,7 @@ test("should display empty state when organization has no members", async () =>
const { organizationInfoCall } = await import("../networking");
(organizationInfoCall as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockOrg);
const user = userEvent.setup();
render(
<OrganizationInfoView
organizationId="org_123"
@@ -117,6 +119,12 @@ test("should display empty state when organization has no members", async () =>
/>,
);
await waitFor(() => {
expect(screen.getByText("Acme Corp")).toBeInTheDocument();
});
await user.click(screen.getByRole("tab", { name: "Members" }));
await waitFor(() => {
expect(screen.getByText("No members found")).toBeInTheDocument();
});
@@ -371,6 +371,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
onAddMember={() => setIsAddMemberModalVisible(true)}
roleColumnTitle="Organization Role"
extraColumns={orgExtraColumns}
emptyText="No members found"
/>
</div>
),