mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(health): stop inheriting configured credentials when a connection test sets its own
A request that supplies its own connection fields describes a connection of its
own, so the configured deployment's credentials are no longer merged underneath
it. Anything the request leaves unset still comes from the configuration, so
naming a configured model and testing it as configured is unchanged, and adding
a second deployment for an already-configured name works as before.
Replaces the earlier outright rejection, which also refused requests that
supplied a complete connection of their own.
(cherry picked from commit b468acb31c)
This commit is contained in:
parent
2653829374
commit
2cf2e037c6
2 changed files with 79 additions and 40 deletions
|
|
@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
|||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Final,
|
||||
Iterable,
|
||||
Literal,
|
||||
Mapping,
|
||||
|
|
@ -55,6 +56,10 @@ from litellm.proxy.middleware.in_flight_requests_middleware import (
|
|||
get_in_flight_requests,
|
||||
)
|
||||
from litellm.proxy.shutdown.graceful_shutdown_manager import GracefulShutdownManager
|
||||
from litellm.router_utils.clientside_credential_handler import (
|
||||
_ADMIN_CONFIG_FIELDS_TO_CLEAR_ON_BASE_OVERRIDE, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the router path
|
||||
clientside_credential_keys,
|
||||
)
|
||||
|
||||
#### Health ENDPOINTS ####
|
||||
|
||||
|
|
@ -92,25 +97,25 @@ 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.
|
||||
_CONFIG_CONNECTION_FIELDS: Final[frozenset[str]] = frozenset(
|
||||
(*_ADMIN_CONFIG_FIELDS_TO_CLEAR_ON_BASE_OVERRIDE, *clientside_credential_keys)
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
def _config_base_for_health_check(
|
||||
config_params: Mapping[str, object], request_params: Mapping[str, object]
|
||||
) -> dict[str, object]:
|
||||
"""Return the configured parameters to merge under a connection-test request.
|
||||
|
||||
A request that sets its own connection fields describes a connection of its
|
||||
own, so the configuration's credentials are not carried into it: they belong
|
||||
to the endpoint the configuration names. Anything the request does not set
|
||||
still comes from the configuration, which is what lets a request name a
|
||||
configured model and test it as configured.
|
||||
"""
|
||||
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."
|
||||
)
|
||||
},
|
||||
)
|
||||
if not any(param in request_params for param in _BANNED_REQUEST_BODY_PARAMS):
|
||||
return dict(config_params)
|
||||
return {key: value for key, value in config_params.items() if key not in _CONFIG_CONNECTION_FIELDS}
|
||||
|
||||
|
||||
def get_callback_identifier(callback):
|
||||
|
|
@ -1894,7 +1899,10 @@ async def test_model_connection(
|
|||
)
|
||||
|
||||
# Merge: config params (from proxy config) as base, request params override
|
||||
litellm_params = {**config_litellm_params, **request_litellm_params}
|
||||
litellm_params = {
|
||||
**_config_base_for_health_check(config_litellm_params, request_litellm_params),
|
||||
**request_litellm_params,
|
||||
}
|
||||
|
||||
## Auth check
|
||||
auth_model_info = loaded_model_info if loaded_model_info is not None else model_info
|
||||
|
|
@ -1908,8 +1916,6 @@ 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={},
|
||||
|
|
|
|||
|
|
@ -2366,30 +2366,63 @@ def test_clean_endpoint_data_strips_credentials_keeps_routing_fields():
|
|||
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."""
|
||||
class TestConfigBaseForHealthCheck:
|
||||
"""A request that sets its own connection fields gets a base without the
|
||||
configuration's credentials; anything it leaves unset still comes from
|
||||
the configuration."""
|
||||
|
||||
def test_banned_param_is_refused(self):
|
||||
from fastapi import HTTPException
|
||||
CONFIG = {
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "sk-configured",
|
||||
"api_base": "https://configured.example/v1",
|
||||
"vertex_credentials": "configured-creds",
|
||||
"rpm": 100,
|
||||
}
|
||||
|
||||
def _base(self, config, request):
|
||||
from litellm.proxy.health_endpoints._health_endpoints import (
|
||||
_reject_banned_param_overrides,
|
||||
_config_base_for_health_check,
|
||||
)
|
||||
|
||||
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)
|
||||
return _config_base_for_health_check(config, request)
|
||||
|
||||
def test_benign_params_are_allowed(self):
|
||||
from litellm.proxy.health_endpoints._health_endpoints import (
|
||||
_reject_banned_param_overrides,
|
||||
def test_request_without_connection_fields_inherits_config(self):
|
||||
base = self._base(self.CONFIG, {"model": "openai/gpt-4o"})
|
||||
assert base["api_key"] == "sk-configured"
|
||||
assert base["api_base"] == "https://configured.example/v1"
|
||||
|
||||
def test_request_setting_api_base_does_not_inherit_config_credentials(self):
|
||||
base = self._base(self.CONFIG, {"api_base": "https://caller.example/v1"})
|
||||
assert "api_key" not in base
|
||||
assert "api_base" not in base
|
||||
assert "vertex_credentials" not in base
|
||||
assert base["rpm"] == 100
|
||||
|
||||
def test_add_model_flow_keeps_its_own_credentials(self):
|
||||
"""Adding a second deployment for an already-configured name sends a
|
||||
complete connection; it is tested as sent, not as configured."""
|
||||
request = {
|
||||
"model": "openai/gpt-4o",
|
||||
"api_base": "https://new-deployment.example/v1",
|
||||
"api_key": "sk-new-deployment",
|
||||
}
|
||||
merged = {**self._base(self.CONFIG, request), **request}
|
||||
assert merged["api_base"] == "https://new-deployment.example/v1"
|
||||
assert merged["api_key"] == "sk-new-deployment"
|
||||
assert "sk-configured" not in str(merged)
|
||||
|
||||
def test_destination_override_without_own_key_inherits_no_credential(self):
|
||||
"""A request that redirects the destination but supplies no credential
|
||||
of its own gets none from the configuration."""
|
||||
request = {"api_base": "https://elsewhere.example"}
|
||||
merged = {**self._base(self.CONFIG, request), **request}
|
||||
assert "api_key" not in merged
|
||||
assert "sk-configured" not in str(merged)
|
||||
|
||||
def test_non_api_base_destination_field_also_drops_credentials(self):
|
||||
base = self._base(
|
||||
{**self.CONFIG, "aws_secret_access_key": "configured-secret"},
|
||||
{"aws_bedrock_runtime_endpoint": "https://caller.example"},
|
||||
)
|
||||
|
||||
_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})
|
||||
assert "api_key" not in base
|
||||
assert "aws_secret_access_key" not in base
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue