Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2edb750
fix: reject model_list in proxy body and gate advisor client credenti…
yucheng-berri Jun 22, 2026
09b9a2a
fix(bedrock): only expand config-sourced AWS credential references (#…
yucheng-berri Jun 22, 2026
53fed7d
fix(proxy): restore admin key/team callback_vars.turn_off_message_log…
yucheng-berri Jul 2, 2026
f270281
fix(anthropic): require caller api_key and SSRF-validate api_base in …
yucheng-berri Jul 4, 2026
485d5c1
fix(proxy): resolve os.environ/ refs universally in DB-sourced models
yucheng-berri Jul 8, 2026
ca97194
chore(proxy): clean up request parameter validation and provider dest…
yucheng-berri Jul 22, 2026
dae4679
fix(proxy)!: apply request-parameter checks consistently across body,…
yuneng-berri Aug 5, 2026
16a630f
build(deps): update ddtrace to the 4.x line
yuneng-berri Aug 8, 2026
29fb706
fix(deps): raise aiohttp floor to 3.14.2 to clear pooled-connection t…
yuneng-berri Aug 8, 2026
35e1bd6
chore(deps): bump mcp to 1.28.1
yuneng-berri Aug 8, 2026
d574d4d
chore(deps): bump pypdf to 6.14.2
yuneng-berri Aug 8, 2026
b4a207f
chore(deps): bump pyasn1 to 0.6.4
yuneng-berri Aug 8, 2026
c3a3263
chore(deps): bump gitpython to 3.1.58
yuneng-berri Aug 8, 2026
28b601c
chore(deps): bump soupsieve to 2.8.4
yuneng-berri Aug 8, 2026
e91c639
chore(deps): bump httplib2 to 0.32.0
yuneng-berri Aug 8, 2026
000ed1c
chore(deps): bump h2 to 4.4.1
yuneng-berri Aug 8, 2026
b400de4
chore(deps): bump langgraph-checkpoint to 4.1.1
yuneng-berri Aug 8, 2026
3f6c0e1
chore(deps): bump setuptools to 83.0.0
yuneng-berri Aug 8, 2026
f3f4ce4
chore(deps): bump cryptography to 50.0.0
yuneng-berri Aug 8, 2026
d664983
chore(deps): bump Pillow to 12.3.0
yuneng-berri Aug 8, 2026
9229782
bump: version 1.89.6 → 1.89.7
yuneng-berri Aug 8, 2026
6842349
chore: refresh uv.lock for 1.89.7
yuneng-berri Aug 8, 2026
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
2 changes: 1 addition & 1 deletion docker/build_from_pip/Dockerfile.build_from_pip
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ RUN uv venv --python python && \
"opentelemetry-api==1.28.0" \
"opentelemetry-sdk==1.28.0" \
"opentelemetry-exporter-otlp==1.28.0" \
"ddtrace==2.19.0" \
"ddtrace==4.11.0" \
"sentry-sdk==2.21.0" \
"mangum==0.17.0" \
"azure-ai-contentsafety==1.0.0" \
Expand Down
2 changes: 1 addition & 1 deletion litellm/litellm_core_utils/dd_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from litellm.secret_managers.main import get_secret_bool

if TYPE_CHECKING:
from ddtrace.tracer import Tracer as DD_TRACER
from ddtrace.trace import Tracer as DD_TRACER
else:
DD_TRACER = Any

Expand Down
29 changes: 20 additions & 9 deletions litellm/litellm_core_utils/initialize_dynamic_callback_params.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
from typing import Dict, Optional
from typing import Any, Dict, Iterator, Optional

from litellm.types.utils import StandardCallbackDynamicParams

_CLIENT_CALLBACK_METADATA_SLOTS: tuple[str, ...] = ("litellm_metadata", "metadata")


def iter_client_callback_metadata_dicts(
kwargs: dict[str, Any],
) -> Iterator[tuple[str, dict[str, Any]]]:
litellm_params = kwargs.get("litellm_params")
if isinstance(litellm_params, dict):
nested = litellm_params.get("metadata")
if isinstance(nested, dict):
yield "litellm_params.metadata", nested
for key in _CLIENT_CALLBACK_METADATA_SLOTS:
candidate = kwargs.get(key)
if isinstance(candidate, dict):
yield key, candidate


def _is_env_reference(value: object) -> bool:
return isinstance(value, str) and "os.environ/" in value
Expand Down Expand Up @@ -57,6 +73,7 @@ def validate_no_callback_env_reference(
"dd_site",
"dd_agent_host",
"dd_agent_port",
"turn_off_message_logging",
]

_request_blocked_callback_params = {
Expand Down Expand Up @@ -91,20 +108,14 @@ def initialize_standard_callback_dynamic_params(
)
standard_callback_dynamic_params[param] = _param_value # type: ignore

# 2. Fallback: check "metadata" or "litellm_params" -> "metadata"
metadata = (kwargs.get("metadata") or {}).copy()
litellm_params = kwargs.get("litellm_params") or {}
if isinstance(litellm_params, dict):
metadata.update(litellm_params.get("metadata") or {})

if isinstance(metadata, dict):
for slot_label, metadata in iter_client_callback_metadata_dicts(kwargs):
for param in _supported_callback_params:
if param in _request_blocked_callback_params:
continue
if param not in standard_callback_dynamic_params and param in metadata:
_param_value = metadata.get(param)
validate_no_callback_env_reference(
param, _param_value, source="metadata"
param, _param_value, source=slot_label
)
standard_callback_dynamic_params[param] = _param_value # type: ignore

Expand Down
12 changes: 12 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,18 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import uuid
from typing import Any, AsyncIterator, Dict, List, Optional, Union

import litellm
import litellm.constants as _c
from litellm.litellm_core_utils.url_utils import validate_url
from litellm.llms.anthropic.common_utils import strip_advisor_blocks_from_messages
from litellm.types.llms.anthropic_messages.anthropic_response import (
AnthropicMessagesResponse,
Expand Down Expand Up @@ -82,10 +84,7 @@ async def handle(
max_uses: int = (
ADVISOR_MAX_USES if _raw_max_uses is None else int(_raw_max_uses)
)
# Optional routing overrides for the advisor sub-call (e.g. proxy routing).
# If not set in the tool definition, litellm resolves from env vars.
advisor_api_key: Optional[str] = advisor_tool.get("api_key")
advisor_api_base: Optional[str] = advisor_tool.get("api_base")
advisor_api_key, advisor_api_base = _resolve_advisor_credentials(advisor_tool)

# Build the synthetic tool definition the provider will receive.
synthetic_advisor_tool = _make_synthetic_advisor_tool()
Expand Down Expand Up @@ -181,6 +180,67 @@ async def handle(
# ---------------------------------------------------------------------------


def _allow_client_side_advisor_credentials() -> bool:
"""Whether a caller-supplied advisor api_base/api_key may be honored.

Gated on the proxy's ``allow_client_side_credentials`` opt-in. When the
interceptor runs outside the proxy (SDK use), there is no admin boundary
to protect, so client-supplied routing is allowed.
"""
try:
from litellm.proxy.proxy_server import general_settings
except (ImportError, ModuleNotFoundError):
return True
return general_settings.get("allow_client_side_credentials") is True


def _resolve_advisor_credentials(
advisor_tool: dict,
) -> tuple[Optional[str], Optional[str]]:
"""Resolve the (api_key, api_base) override for the advisor sub-call.

A caller-supplied ``api_base`` is only honored alongside a caller-supplied
``api_key``: without one, ``AnthropicModelInfo.get_auth_header()`` falls
back to the proxy's own Anthropic credentials, which would then be sent to
the caller-chosen ``api_base``. A caller-supplied ``api_base`` is also
required to be https with TLS verification on, and SSRF-validated so it
can't target a private/internal/cloud-metadata address, mirroring
``proxy.auth.auth_utils.check_complete_credentials``. https with TLS
verification is required because ``validate_url`` only rewrites the
connection to a DNS-pinned IP for http, or for https with
``litellm.ssl_verify`` disabled; otherwise it returns the URL unchanged
and relies on certificate validation to block DNS rebinding, so this
closes the same gap without threading the pinned URL through the whole
``anthropic_messages()`` call chain.
"""
if not _allow_client_side_advisor_credentials():
return None, None
api_key: Optional[str] = advisor_tool.get("api_key")
api_base: Optional[str] = advisor_tool.get("api_base")
if api_base is None:
return api_key, None
if not api_key:
raise ValueError(
"advisor tool definition sets 'api_base' without 'api_key'. A "
"caller-supplied api_base is only honored alongside a "
"caller-supplied api_key, so the proxy's own credentials are "
"never sent to a caller-chosen destination."
)
if not api_base.startswith("https://"):
raise ValueError(
f"advisor tool definition sets 'api_base'={api_base!r}, which must use the https scheme."
)
if getattr(litellm, "ssl_verify", True) is False:
raise ValueError(
"advisor tool definition sets 'api_base' but the proxy has TLS verification "
"disabled (litellm.ssl_verify=False), so a caller-supplied api_base can't be "
"safely validated against DNS rebinding."
)
if getattr(litellm, "user_url_validation", True):
validate_url(api_base)
return api_key, api_base


def _make_synthetic_advisor_tool() -> Dict:
"""Build a regular tool definition the executor provider can understand."""
return {
Expand Down
62 changes: 34 additions & 28 deletions litellm/llms/bedrock/base_aws_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
Callable,
ClassVar,
Dict,
List,
Literal,
Optional,
Tuple,
Expand Down Expand Up @@ -210,32 +209,11 @@ def get_credentials(
"""
Return a boto3.Credentials object
"""
## CHECK IS 'os.environ/' passed in
params_to_check: List[Optional[str]] = [
aws_access_key_id,
aws_secret_access_key,
aws_session_token,
aws_region_name,
aws_session_name,
aws_profile_name,
aws_role_name,
aws_web_identity_token,
aws_sts_endpoint,
aws_external_id,
]

# Iterate over parameters and update if needed
for i, param in enumerate(params_to_check):
if param and param.startswith("os.environ/"):
_v = get_secret(param)
if _v is not None and isinstance(_v, str):
params_to_check[i] = _v
elif param is None: # check if uppercase value in env
key = self.aws_authentication_params[i]
if key.upper() in os.environ:
params_to_check[i] = os.getenv(key.upper())

# Assign updated values back to parameters
# Only config-sourced credentials are expanded against the environment.
# os.environ/<VAR> references in the model config are resolved at load time,
# so any reference still present at this point is caller-supplied input and is
# left as-is rather than expanded into a process environment variable. Each
# unset param falls back to its matching fixed AWS_* ambient env var.
(
aws_access_key_id,
aws_secret_access_key,
Expand All @@ -247,7 +225,21 @@ def get_credentials(
aws_web_identity_token,
aws_sts_endpoint,
aws_external_id,
) = params_to_check
) = tuple(
value if value is not None else os.getenv(env_var)
for value, env_var in (
(aws_access_key_id, "AWS_ACCESS_KEY_ID"),
(aws_secret_access_key, "AWS_SECRET_ACCESS_KEY"),
(aws_session_token, "AWS_SESSION_TOKEN"),
(aws_region_name, "AWS_REGION_NAME"),
(aws_session_name, "AWS_SESSION_NAME"),
(aws_profile_name, "AWS_PROFILE_NAME"),
(aws_role_name, "AWS_ROLE_NAME"),
(aws_web_identity_token, "AWS_WEB_IDENTITY_TOKEN"),
(aws_sts_endpoint, "AWS_STS_ENDPOINT"),
(aws_external_id, "AWS_EXTERNAL_ID"),
)
)

verbose_logger.debug(
"in get credentials\n"
Expand Down Expand Up @@ -845,6 +837,20 @@ def _auth_with_web_identity_token(
f"IN Web Identity Token: {aws_web_identity_token} | Role Name: {aws_role_name} | Session Name: {aws_session_name}"
)

# get_secret() expands environment-variable references (an os.environ/<VAR>
# prefix, or a bare name matching an environment variable). Config-sourced
# references are expanded at load time, so such a reference reaching here is
# caller-supplied input; reject it rather than expanding a process-environment
# value for use as the token.
if (
aws_web_identity_token.startswith("os.environ/")
or aws_web_identity_token in os.environ
):
raise AwsAuthError(
message="Invalid web identity token reference.",
status_code=400,
)

oidc_token = get_secret(aws_web_identity_token)

if oidc_token is None:
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 @@ -348,7 +348,7 @@ def embedding(
)
# 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 @@ -330,25 +330,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
Loading
Loading