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:
Yuneng Jiang 2026-08-05 15:48:16 -07:00
parent 2653829374
commit 2cf2e037c6
No known key found for this signature in database
2 changed files with 79 additions and 40 deletions

View file

@ -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={},

View file

@ -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