diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 4250b9668ff..64a47d5fe81 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -6,7 +6,17 @@ import secrets import time import traceback from datetime import datetime, timedelta -from typing import Any, Dict, Iterable, Literal, Optional, TypedDict, Union, cast +from typing import ( + Any, + Dict, + Iterable, + Literal, + Mapping, + Optional, + TypedDict, + Union, + cast, +) import fastapi from fastapi import APIRouter, Depends, HTTPException, Request, Response, status @@ -28,6 +38,9 @@ from litellm.proxy._types import ( UserAPIKeyAuth, WebhookEvent, ) +from litellm.proxy.auth.auth_utils import ( + _BANNED_REQUEST_BODY_PARAMS, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the request-body check +) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.health_check import ( @@ -79,6 +92,27 @@ def _reject_os_environ_references(params: dict) -> None: stack.append(value) +def _reject_banned_param_overrides(request_params: Mapping[str, object]) -> None: + """Reject request params that would replace a configured deployment's routing or credentials. + + Applied only when a configured deployment supplies the base parameters. The + request may still adjust benign fields; routing and credential fields come + from the configuration. A caller who wants a fully custom connection supplies + the complete parameter set instead of naming a configured model. + """ + for param in _BANNED_REQUEST_BODY_PARAMS: + if param in request_params: + raise HTTPException( + status_code=400, + detail={ + "error": ( + f"{param} cannot be overridden when testing a configured model. " + "Provide the full connection parameters instead of naming a configured model." + ) + }, + ) + + def get_callback_identifier(callback): """ Get the callback identifier string, handling both strings and objects. @@ -1860,7 +1894,6 @@ async def test_model_connection( ) # Merge: config params (from proxy config) as base, request params override - # This allows users to override specific params while using config for credentials litellm_params = {**config_litellm_params, **request_litellm_params} ## Auth check @@ -1875,6 +1908,8 @@ async def test_model_connection( prisma_client=prisma_client, premium_user=premium_user, ) + if config_litellm_params: + _reject_banned_param_overrides(request_litellm_params) # Include health_check_params if provided litellm_params = _update_litellm_params_for_health_check( model_info={}, diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index 917bedcb93f..7280abd8ea5 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -2364,3 +2364,32 @@ def test_clean_endpoint_data_strips_credentials_keeps_routing_fields(): assert "aws_access_key_id" not in cleaned assert cleaned.get("api_base") == "https://example.test/v1" assert cleaned.get("api_version") == "2024-10-21" + + +class TestRejectBannedParamOverrides: + """Routing and credential fields come from the deployment configuration when a + request names a configured model; a request that wants its own connection + supplies the whole parameter set instead.""" + + def test_banned_param_is_refused(self): + from fastapi import HTTPException + + from litellm.proxy.health_endpoints._health_endpoints import ( + _reject_banned_param_overrides, + ) + + for param in ("api_base", "base_url", "vertex_credentials", "aws_web_identity_token"): + with pytest.raises(HTTPException) as exc_info: + _reject_banned_param_overrides({"model": "gpt-4o", param: "caller-supplied"}) + assert exc_info.value.status_code == 400 + assert param in str(exc_info.value.detail) + + def test_benign_params_are_allowed(self): + from litellm.proxy.health_endpoints._health_endpoints import ( + _reject_banned_param_overrides, + ) + + _reject_banned_param_overrides({}) + _reject_banned_param_overrides({"model": "gpt-4o"}) + _reject_banned_param_overrides({"model": "gpt-4o", "api_key": "sk-caller-owned"}) + _reject_banned_param_overrides({"mode": "chat", "timeout": 30})