mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
Merge 9e36c2752f into f4308bc124
This commit is contained in:
commit
b388373485
2 changed files with 142 additions and 2 deletions
|
|
@ -73,6 +73,7 @@ from litellm.router_utils.clientside_credential_handler import (
|
|||
clientside_credential_keys,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_bool
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
#### Health ENDPOINTS ####
|
||||
|
||||
|
|
@ -125,6 +126,15 @@ _CONFIG_CONNECTION_FIELDS: Final[frozenset[str]] = frozenset(
|
|||
)
|
||||
)
|
||||
|
||||
# The banned request-body params that actually describe a CONNECTION — the
|
||||
# banned list minus the custom-pricing fields. Pricing fields are banned from a
|
||||
# request body because they poison the shared model-cost registry, not because
|
||||
# they retarget or re-authenticate the outbound call, so the full list would
|
||||
# treat `input_cost_per_token` as a credential.
|
||||
_CONNECTION_OVERRIDE_REQUEST_PARAMS: Final[tuple[str, ...]] = tuple(
|
||||
param for param in _BANNED_REQUEST_BODY_PARAMS if param not in CustomPricingLiteLLMParams.model_fields
|
||||
)
|
||||
|
||||
|
||||
def _request_inherits_config_credentials(
|
||||
config_params: Mapping[str, object],
|
||||
|
|
@ -140,13 +150,21 @@ def _request_inherits_config_credentials(
|
|||
name is no name: ``load_credentials_from_list`` resolves nothing from it, so
|
||||
it must not cost the request the credentials it would otherwise be probed
|
||||
with.
|
||||
|
||||
The trigger is ``_CONNECTION_OVERRIDE_REQUEST_PARAMS``, not the full banned
|
||||
list: the custom-pricing fields are banned from a request body for a
|
||||
different reason (they poison the shared model-cost registry) and say
|
||||
nothing about which connection a test describes. Treating them as a
|
||||
connection override empties the configuration under a request that only
|
||||
named a model and its price, which reports a healthy deployment as
|
||||
"Missing credentials".
|
||||
"""
|
||||
requested_credential: Final = request_params.get("litellm_credential_name")
|
||||
if requested_credential and requested_credential != config_params.get("litellm_credential_name"):
|
||||
return False
|
||||
if allow_client_side_credentials:
|
||||
return True
|
||||
return not any(param in request_params for param in _BANNED_REQUEST_BODY_PARAMS)
|
||||
return not any(param in request_params for param in _CONNECTION_OVERRIDE_REQUEST_PARAMS)
|
||||
|
||||
|
||||
def _config_base_for_health_check(
|
||||
|
|
@ -2205,10 +2223,16 @@ async def test_model_connection(
|
|||
premium_user=premium_user,
|
||||
)
|
||||
mode = mode or litellm_params.pop("mode", None)
|
||||
# A connection test needs no prices, and ``completion`` registers any
|
||||
# request pricing under the shared ``{provider}/{model}`` cost-map key
|
||||
# (a probe carries no router deployment id), which would re-price every
|
||||
# sibling deployment of the same backend model. Probe without them,
|
||||
# whether they came from the request or from the configuration.
|
||||
probe_params: Final = CustomPricingLiteLLMParams.strip_custom_pricing_fields(litellm_params)
|
||||
|
||||
result: Final = await run_with_timeout(
|
||||
litellm.ahealth_check(
|
||||
model_params=litellm_params,
|
||||
model_params=probe_params,
|
||||
mode=mode,
|
||||
prompt="test from litellm",
|
||||
input=["test from litellm"],
|
||||
|
|
|
|||
|
|
@ -3808,6 +3808,50 @@ class TestConfigBaseForHealthCheck:
|
|||
assert "litellm_credential_name" not in base
|
||||
assert "api_key" not in base
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pricing_field,value",
|
||||
[
|
||||
("input_cost_per_token", 1e-9),
|
||||
("output_cost_per_token", 2e-9),
|
||||
("cache_read_input_token_cost", 5e-10),
|
||||
],
|
||||
)
|
||||
def test_pricing_field_is_not_a_connection_override(self, pricing_field, value):
|
||||
"""Pricing fields are banned from a request body because they poison the
|
||||
shared model-cost registry, not because they describe a connection. A
|
||||
connection test that carries one still gets the configured credentials —
|
||||
otherwise a healthy deployment reports "Missing credentials"."""
|
||||
base = self._base(self.CONFIG, {"model": "openai/gpt-4o", pricing_field: value})
|
||||
assert base["api_key"] == "sk-configured"
|
||||
assert base["api_base"] == "https://configured.example/v1"
|
||||
|
||||
def test_pricing_field_alongside_a_real_override_still_drops_credentials(self):
|
||||
"""The pricing field is neutral, so the api_base beside it still decides."""
|
||||
base = self._base(
|
||||
self.CONFIG,
|
||||
{"api_base": "https://caller.example/v1", "input_cost_per_token": 1e-9},
|
||||
)
|
||||
assert "api_key" not in base
|
||||
assert "sk-configured" not in str(base)
|
||||
|
||||
def test_every_custom_pricing_field_is_excluded_from_the_connection_list(self):
|
||||
from litellm.proxy.health_endpoints._health_endpoints import _CONNECTION_OVERRIDE_REQUEST_PARAMS
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
connection_params = set(_CONNECTION_OVERRIDE_REQUEST_PARAMS)
|
||||
for field in CustomPricingLiteLLMParams.model_fields:
|
||||
assert field not in connection_params, (
|
||||
f"CustomPricingLiteLLMParams.{field} is treated as a connection override, "
|
||||
"so a connection test that sets it loses the configured credentials."
|
||||
)
|
||||
|
||||
def test_connection_list_keeps_the_real_credential_and_endpoint_fields(self):
|
||||
from litellm.proxy.health_endpoints._health_endpoints import _CONNECTION_OVERRIDE_REQUEST_PARAMS
|
||||
|
||||
connection_params = set(_CONNECTION_OVERRIDE_REQUEST_PARAMS)
|
||||
for field in ("api_base", "base_url", "azure_ad_token", "vertex_credentials", "user_config"):
|
||||
assert field in connection_params
|
||||
|
||||
def test_stored_credential_reference_kept_when_request_sets_no_connection(self):
|
||||
"""The Admin UI tests a configured model by naming it plus its stored
|
||||
credential and nothing else; that keeps working."""
|
||||
|
|
@ -3986,6 +4030,78 @@ class TestTestConnectionUsesTheNamedCredential:
|
|||
assert probe.headers["authorization"] == f"Bearer {self.CREDENTIAL_KEY}"
|
||||
|
||||
|
||||
class TestTestConnectionLeavesSharedPricingAlone:
|
||||
"""A connection test must not write prices into ``litellm.model_cost``: a probe
|
||||
carries no router deployment id, so ``completion`` would register them under
|
||||
the shared ``{provider}/{model}`` key that sibling deployments price from."""
|
||||
|
||||
MODEL = "openai/test-connection-pricing-probe"
|
||||
SHARED_KEY = MODEL
|
||||
CONFIG_KEY = "sk-configured"
|
||||
COMPLETION = TestTestConnectionUsesTheNamedCredential.COMPLETION
|
||||
|
||||
def _probe(self, monkeypatch, config_litellm_params: dict, request_litellm_params: dict) -> httpx.Request:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setattr(litellm, "model_cost", dict(litellm.model_cost))
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(_health_endpoints_module.router)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
|
||||
router = MagicMock()
|
||||
router.get_deployment.return_value = None
|
||||
router.get_model_list.return_value = [
|
||||
{"model_name": "pricing-probe", "litellm_params": config_litellm_params, "model_info": {}}
|
||||
]
|
||||
|
||||
with (
|
||||
patch( # test-quality-ok: the endpoint reads the proxy-global DB client and 500s when it is None; it has no injection seam
|
||||
"litellm.proxy.proxy_server.prisma_client", MagicMock()
|
||||
),
|
||||
patch( # test-quality-ok: the deployment the probe is matched against is a proxy global; it has no injection seam
|
||||
"litellm.proxy.proxy_server.llm_router", router
|
||||
),
|
||||
respx.mock(assert_all_called=True) as respx_mock,
|
||||
):
|
||||
respx_mock.post(path__regex=r".*/chat/completions").respond(json=self.COMPLETION)
|
||||
response = TestClient(app).post(
|
||||
"/health/test_connection",
|
||||
json={"mode": "chat", "litellm_params": request_litellm_params, "model_info": {"mode": "chat"}},
|
||||
)
|
||||
probe = respx_mock.calls.last.request
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["status"] == "success", response.text
|
||||
return probe
|
||||
|
||||
def test_request_pricing_is_not_registered_under_the_shared_key(self, monkeypatch):
|
||||
probe = self._probe(
|
||||
monkeypatch,
|
||||
{"model": self.MODEL, "api_key": self.CONFIG_KEY, "api_base": "https://configured.example/v1"},
|
||||
{"model": self.MODEL, "input_cost_per_token": 1.0, "output_cost_per_token": 2.0},
|
||||
)
|
||||
|
||||
assert probe.headers["authorization"] == f"Bearer {self.CONFIG_KEY}"
|
||||
assert self.SHARED_KEY not in litellm.model_cost
|
||||
|
||||
def test_configured_pricing_is_not_registered_under_the_shared_key(self, monkeypatch):
|
||||
probe = self._probe(
|
||||
monkeypatch,
|
||||
{
|
||||
"model": self.MODEL,
|
||||
"api_key": self.CONFIG_KEY,
|
||||
"api_base": "https://configured.example/v1",
|
||||
"input_cost_per_token": 3.0,
|
||||
"output_cost_per_token": 4.0,
|
||||
},
|
||||
{"model": self.MODEL},
|
||||
)
|
||||
|
||||
assert probe.url.host == "configured.example"
|
||||
assert self.SHARED_KEY not in litellm.model_cost
|
||||
|
||||
|
||||
class TestNoRedisWarning:
|
||||
"""`show_no_redis_warning` drives the Admin UI's default-on "no Redis" banner."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue