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: 9 additions & 0 deletions litellm/litellm_core_utils/url_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ def _parse_url_destination_allowlist_entry(
return _normalize_host(parsed.hostname), scheme, port


def provider_url_destination_candidates(value: str) -> Tuple[str, ...]:
return tuple(
candidate
for part in value.split(",")
for candidate in (part.strip(), part.strip().split("/", 1)[1] if "/" in part.strip() else "")
if candidate
)


def is_url_destination_allowed_by_host(url: str, allowed_hosts: List[str]) -> bool:
"""Return True when a credential-bearing provider URL is admin-allowlisted.

Expand Down
2 changes: 1 addition & 1 deletion litellm/llms/huggingface/embedding/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def embedding(
task = get_hf_task_embedding_for_model(model=model, task_type=task_type, api_base=HF_HUB_URL)
# print_verbose(f"{model}, {task}")
embed_url = ""
if "https" in model:
if model.startswith(("http://", "https://")):
embed_url = model
elif api_base:
embed_url = api_base
Expand Down
19 changes: 0 additions & 19 deletions litellm/llms/huggingface/embedding/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,25 +316,6 @@ def transform_request(

return data

def get_api_base(self, api_base: Optional[str], model: str) -> str:
"""
Get the API base for the Huggingface API.

Do not add the chat/embedding/rerank extension here. Let the handler do this.
"""
if "https" in model:
completion_url = model
elif api_base is not None:
completion_url = api_base
elif "HF_API_BASE" in os.environ:
completion_url = os.getenv("HF_API_BASE", "")
elif "HUGGINGFACE_API_BASE" in os.environ:
completion_url = os.getenv("HUGGINGFACE_API_BASE", "")
else:
completion_url = f"https://api-inference.huggingface.co/models/{model}"

return completion_url

def validate_environment(
self,
headers: Dict,
Expand Down
4 changes: 2 additions & 2 deletions litellm/llms/oobabooga/chat/oobabooga.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def completion(
optional_params=optional_params,
litellm_params=litellm_params,
)
if "https" in model:
if model.startswith(("http://", "https://")):
completion_url = model
elif api_base:
completion_url = api_base
Expand Down Expand Up @@ -96,7 +96,7 @@ def embedding(
encoding=None,
):
# Create completion URL
if "https" in model:
if model.startswith(("http://", "https://")):
embeddings_url = model
elif api_base:
embeddings_url = f"{api_base}/v1/embeddings"
Expand Down
80 changes: 78 additions & 2 deletions litellm/proxy/auth/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
from functools import lru_cache
from logging import Logger
from typing import Any, Dict, FrozenSet, List, Mapping, Optional, Tuple, Union
from typing import Any, Dict, FrozenSet, Iterator, List, Mapping, Optional, Tuple, Union

from fastapi import HTTPException, Request, status

Expand All @@ -12,8 +12,14 @@
from litellm._logging import verbose_proxy_logger
from litellm.constants import STANDARD_CUSTOMER_ID_HEADERS
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
from litellm.litellm_core_utils.url_utils import SSRFError, validate_url
from litellm.litellm_core_utils.url_utils import (
SSRFError,
is_url_destination_allowed_by_host,
provider_url_destination_candidates,
validate_url,
)
from litellm.proxy._types import *
from litellm.proxy.common_utils.http_parsing_utils import extract_nested_form_metadata
from litellm.types.router import CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS
from litellm.types.utils import CustomPricingLiteLLMParams

Expand Down Expand Up @@ -286,6 +292,7 @@ def _build_banned_observability_params() -> FrozenSet[str]:
"use_ssl",
# SDK-only field; also rejected outright in is_request_body_safe.
"model_list",
"vertex_ai_credentials",
# Observability credentials, hosts, and project identifiers: derived
# from the canonical ``_supported_callback_params`` allowlist so new
# integrations are covered automatically. Sorted for stable iteration
Expand Down Expand Up @@ -338,6 +345,60 @@ def _check_banned_params(
)


_FALLBACK_FIELDS: tuple[str, ...] = (
"fallbacks",
"context_window_fallbacks",
"content_policy_fallbacks",
)


def _iter_fallback_field_values(request_body: Mapping[str, object]) -> Iterator[object]:
override = request_body.get("router_settings_override")
for source in (request_body, override):
if isinstance(source, Mapping):
for field in _FALLBACK_FIELDS:
yield source.get(field)


def _iter_fallback_targets(value: object, depth: int) -> Iterator[str | Mapping[str, object]]:
if depth > 2 * litellm.ROUTER_MAX_FALLBACKS:
raise ValueError("Rejected Request: fallback nesting exceeds the allowed validation depth.")
if not isinstance(value, list):
return
for item in value:
if isinstance(item, str):
yield item
elif isinstance(item, Mapping):
values = tuple(item.values())
if not (values and all(isinstance(v, list) for v in values)):
yield item
if isinstance(item.get("model"), str):
for field in _FALLBACK_FIELDS:
yield from _iter_fallback_targets(item.get(field), depth + 1)
else:
for target_list in values:
yield from _iter_fallback_targets(target_list, depth + 1)


def iter_request_fallback_targets(request_body: Mapping[str, object]) -> Iterator[str | Mapping[str, object]]:
for value in _iter_fallback_field_values(request_body):
yield from _iter_fallback_targets(value, 0)


def _reject_url_valued_fallback_target(value: str) -> None:
allowed_hosts = getattr(litellm, "provider_url_destination_allowed_hosts", []) or []
for candidate in provider_url_destination_candidates(value):
if not candidate.lower().startswith(("http://", "https://")):
continue
if is_url_destination_allowed_by_host(candidate, allowed_hosts):
continue
raise ValueError(
f"Rejected Request: URL-valued fallback destination '{value}' is not allowed. "
"Configure custom endpoints with api_base instead, or add the destination host to "
"`provider_url_destination_allowed_hosts` in litellm_settings."
)


def is_request_body_safe(request_body: dict, general_settings: dict, llm_router: Optional[Router], model: str) -> bool:
"""
Check if the request body is safe.
Expand Down Expand Up @@ -375,6 +436,21 @@ def is_request_body_safe(request_body: dict, general_settings: dict, llm_router:
metadata = _coerce_metadata_to_dict(request_body.get(metadata_key))
if metadata is not None:
_check_banned_params(metadata, general_settings, llm_router, model)
if any(isinstance(key, str) and key.startswith(f"{metadata_key}[") for key in request_body):
_check_banned_params(
extract_nested_form_metadata(form_data=request_body, prefix=f"{metadata_key}["),
general_settings,
llm_router,
model,
)
for target in iter_request_fallback_targets(request_body):
if isinstance(target, dict):
_check_banned_params(target, general_settings, llm_router, model)
target_model = target.get("model")
if isinstance(target_model, str):
_reject_url_valued_fallback_target(target_model)
elif isinstance(target, str):
_reject_url_valued_fallback_target(target)
litellm_params = _coerce_metadata_to_dict(request_body.get("litellm_params"))
if litellm_params is not None:
litellm_params_metadata = _coerce_metadata_to_dict(litellm_params.get("metadata"))
Expand Down
59 changes: 15 additions & 44 deletions litellm/proxy/auth/user_api_key_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import orjson
from datetime import datetime, timezone
from typing import Any, Dict, Iterator, NamedTuple, List, Optional, Protocol, Tuple, Union, cast
from typing import Any, Dict, NamedTuple, List, Optional, Protocol, Tuple, Union, cast

import fastapi
from fastapi import HTTPException, Request, WebSocket, status
Expand Down Expand Up @@ -58,6 +58,7 @@
get_model_from_request,
get_request_route,
get_request_route_template,
iter_request_fallback_targets,
normalize_request_route,
pre_db_read_auth_checks,
route_in_additonal_public_routes,
Expand Down Expand Up @@ -2794,19 +2795,11 @@ async def _enforce_key_and_fallback_model_access(
llm_router=llm_router,
)

# Validate every fallback model name reachable by this request.
# All three fields (``fallbacks``, ``context_window_fallbacks``,
# ``content_policy_fallbacks``) are forwarded to the router as
# per-request kwargs whether they appear at the top level of
# ``request_data`` or nested under ``router_settings_override``.
# Both surfaces must be validated against the API key's model
# allowlist or a caller can smuggle a restricted model. VERIA-44.
fallback_names: List[str] = []
override_settings = request_data.get("router_settings_override")
for _fb_key in ROUTER_FALLBACK_FIELDS:
fallback_names.extend(iter_router_fallback_model_names(request_data.get(_fb_key)))
if isinstance(override_settings, dict):
fallback_names.extend(iter_router_fallback_model_names(override_settings.get(_fb_key)))
fallback_names = tuple(
name
for target in iter_request_fallback_targets(request_data)
if (name := _fallback_target_model_name(target)) is not None
)

for _name in dict.fromkeys(fallback_names): # dedupe, preserve order
await can_key_call_model(
Expand All @@ -2822,36 +2815,14 @@ async def _enforce_key_and_fallback_model_access(
)


ROUTER_FALLBACK_FIELDS: Tuple[str, ...] = (
"fallbacks",
"context_window_fallbacks",
"content_policy_fallbacks",
)


def iter_router_fallback_model_names(fallbacks: Any) -> Iterator[str]:
"""Yield leaf model names from any of the supported fallbacks shapes.

Handles the simple top-level shape (``str`` or ``{"model": str}``) and
the nested router-config shape (``[{primary: [fallback_list]}]``).
"""
if not isinstance(fallbacks, list):
return
for entry in fallbacks:
if isinstance(entry, str):
yield entry
elif isinstance(entry, dict):
if isinstance(entry.get("model"), str):
yield entry["model"]
continue
for fallback_list in entry.values():
if not isinstance(fallback_list, list):
continue
for m in fallback_list:
if isinstance(m, str):
yield m
elif isinstance(m, dict) and isinstance(m.get("model"), str):
yield m["model"]
def _fallback_target_model_name(target: object) -> str | None:
if isinstance(target, str):
return target
if isinstance(target, dict):
model = target.get("model")
if isinstance(model, str):
return model
return None


async def _run_post_custom_auth_checks(
Expand Down
8 changes: 7 additions & 1 deletion litellm/proxy/common_request_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@
ProxyConfig = _ProxyConfig
else:
ProxyConfig = Any
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
from litellm.proxy.litellm_pre_call_utils import (
add_litellm_data_to_request,
reject_url_valued_destination,
)
from litellm.types.utils import (
ModelResponse,
ModelResponseStream,
Expand Down Expand Up @@ -1106,6 +1109,9 @@ async def common_processing_pre_call_logic(
self.data[_metadata_variable_name] = {}
self.data[_metadata_variable_name]["queue_time_seconds"] = queue_time_seconds

if isinstance(model, str):
reject_url_valued_destination("model", model)

self.data["model"] = (
general_settings.get("completion_model", None) # server default
or user_model # model name passed via cli args
Expand Down
Loading
Loading