mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(health): don't treat a custom-pricing field as a connection override
/health/test_connection returns "Missing credentials. Please pass one of
api_key, azure_ad_token, ..." for a perfectly healthy configured model, as
soon as the request also carries any custom-pricing field.
_config_base_for_health_check strips the configured connection fields when
the request sets one of _BANNED_REQUEST_BODY_PARAMS, on the reasoning that
such a request describes a connection of its own. That tuple ends with
every CustomPricingLiteLLMParams field, which is correct for the request-body
check it was built for — those fields are banned because they poison the
shared model-cost registry — but they are not connection parameters. A test
that names a configured model and its price therefore has the configuration
emptied out from under it, litellm_credential_name included, and the probe
fails with no credential at all.
It lands on the Admin UI's Add Model wizard, where "Test Connection" sits
next to the pricing fields that a model missing from the cost map has to
have, so the two are filled in together and the button reports a working
deployment as broken.
Splits the connection-relevant subset out as
_CONNECTION_OVERRIDE_REQUEST_PARAMS — the same tuple minus the pricing
fields — and gates the health-check merge on that. The request-body check is
unchanged, so pricing fields stay banned there.
Measured against a configured azure deployment: {"model": ...} succeeds,
{"model": ..., "input_cost_per_token": 1e-9} failed and now succeeds, and
{"api_base": ..., "input_cost_per_token": 1e-9} still drops the configured
credentials.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
680233fa8b
commit
fa4a28eca0
3 changed files with 65 additions and 2 deletions
|
|
@ -371,6 +371,17 @@ _BANNED_REQUEST_BODY_PARAMS: Final[tuple[str, ...]] = (
|
|||
)
|
||||
|
||||
|
||||
# The banned params that actually describe a CONNECTION — the tuple above 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. A caller that needs to reason about
|
||||
# "did this request bring its own connection?" must use this list; the full
|
||||
# tuple 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 _check_banned_params(
|
||||
body: dict,
|
||||
general_settings: dict,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ from litellm.proxy.auth.auth_checks import (
|
|||
_resolve_key_models_for_auth_check, # pyright: ignore[reportPrivateUsage] # the auth layer's sentinel resolution, reused so /health scopes exactly like a request
|
||||
)
|
||||
from litellm.proxy.auth.auth_utils import (
|
||||
_BANNED_REQUEST_BODY_PARAMS, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the request-body check
|
||||
_CONNECTION_OVERRIDE_REQUEST_PARAMS, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the request-body check
|
||||
)
|
||||
from litellm.proxy.auth.model_checks import get_key_models
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
|
@ -139,13 +139,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(
|
||||
|
|
|
|||
|
|
@ -3680,6 +3680,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.auth.auth_utils 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.auth.auth_utils 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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue