mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(router): keep unresolved drop_params strings so DB rows and env refs survive
The drop_params validator collapsed every string it did not recognize to None. A pre-fix DB row holds the flag as ciphertext, so a partial PATCH rebuilt the deployment without it and dropped the key from the stored row, and /model/new turned an os.environ/ reference into nothing before the loader could resolve it. The validator now returns the raw value when it is not a boolean flag, the field admits strings the way timeout already does, and the flag set follows pydantic's lax bool parsing instead of a hand-rolled true/false pair
This commit is contained in:
parent
b827375e60
commit
4cc0180eab
7 changed files with 101 additions and 16 deletions
|
|
@ -5,6 +5,7 @@ from collections.abc import Iterable, Mapping
|
|||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.types.llms.openai import AllMessageValues, OpenAIChatCompletionFinishReason
|
||||
|
|
@ -37,16 +38,16 @@ def safe_divide_seconds(seconds: float, denominator: float, default: float | Non
|
|||
return float(seconds / denominator)
|
||||
|
||||
|
||||
_DROP_PARAMS_BOOL: Final = TypeAdapter(bool)
|
||||
|
||||
|
||||
def normalize_drop_params(value: object) -> bool | None:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
lowered: Final = value.strip().lower()
|
||||
if lowered == "true":
|
||||
return True
|
||||
if lowered == "false":
|
||||
return False
|
||||
return None
|
||||
try:
|
||||
return _DROP_PARAMS_BOOL.validate_python(value.strip() if isinstance(value, str) else value)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
||||
def safe_divide(
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
|
|||
timeout: float | str | httpx.Timeout | None = None # if str, pass in as os.environ/
|
||||
stream_timeout: float | str | None = None # timeout when making stream=True calls, if str, pass in as os.environ/
|
||||
max_retries: int | None = None
|
||||
drop_params: bool | None = None
|
||||
drop_params: bool | str | None = None
|
||||
organization: str | None = None # for openai orgs
|
||||
configurable_clientside_auth_params: CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS = None
|
||||
litellm_credential_name: str | None = None
|
||||
|
|
@ -408,8 +408,9 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
|
|||
|
||||
@field_validator("drop_params", mode="before")
|
||||
@classmethod
|
||||
def coerce_drop_params(cls, value: object) -> bool | None:
|
||||
return normalize_drop_params(value)
|
||||
def coerce_drop_params(cls, value: object) -> object:
|
||||
normalized: Final = normalize_drop_params(value)
|
||||
return value if normalized is None else normalized
|
||||
|
||||
def __contains__(self, key) -> bool:
|
||||
# Define custom behavior for the 'in' operator
|
||||
|
|
|
|||
|
|
@ -268,11 +268,16 @@ class TestRedactNestedMatchAndRegexKeys:
|
|||
(" TRUE ", True),
|
||||
("false", False),
|
||||
("False", False),
|
||||
("yes", True),
|
||||
("off", False),
|
||||
("1", True),
|
||||
(1, True),
|
||||
(0, False),
|
||||
(None, None),
|
||||
("yes", None),
|
||||
("", None),
|
||||
(1, None),
|
||||
(0, None),
|
||||
("os.environ/DROP_PARAMS", None),
|
||||
("v2:gcm:not-a-flag", None),
|
||||
(2, None),
|
||||
],
|
||||
)
|
||||
def test_normalize_drop_params(value, expected):
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from litellm.proxy.management_endpoints.model_management_endpoints import (
|
|||
)
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
from litellm.router import Router
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, updateDeployment
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo, updateDeployment, updateLiteLLMParams
|
||||
|
||||
|
||||
async def _passthrough_row(update_data):
|
||||
|
|
@ -3070,6 +3070,31 @@ class TestUpdateDBModelBlocked:
|
|||
assert "blocked" not in result
|
||||
|
||||
|
||||
class TestUpdateDBModelKeepsLegacyDropParams:
|
||||
def test_partial_patch_keeps_encrypted_string_drop_params(self, monkeypatch):
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model
|
||||
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-1234")
|
||||
legacy_row = Deployment(
|
||||
model_name="gpt-5-nano",
|
||||
litellm_params=LiteLLM_Params(
|
||||
model="openai/gpt-5-nano",
|
||||
api_key=encrypt_value_helper(value="sk-old"),
|
||||
drop_params=encrypt_value_helper(value="true"),
|
||||
),
|
||||
model_info=ModelInfo(id="legacy-row"),
|
||||
)
|
||||
|
||||
result = update_db_model(
|
||||
db_model=legacy_row,
|
||||
updated_patch=updateDeployment(litellm_params=updateLiteLLMParams(api_key="sk-new")),
|
||||
)
|
||||
|
||||
stored = json.loads(result["litellm_params"])
|
||||
assert decrypt_value_helper(value=stored["drop_params"], key="drop_params") == "true"
|
||||
|
||||
|
||||
def _build_db_model_with_pricing():
|
||||
"""Wildcard deployment with custom pricing in litellm_params; Deployment.__init__
|
||||
mirrors SPECIAL_MODEL_INFO_PARAMS into model_info, so both blobs hold the rate."""
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import pytest
|
|||
|
||||
import litellm
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
|
||||
from litellm.proxy.proxy_server import (
|
||||
ProxyConfig,
|
||||
_is_remote_module_url,
|
||||
|
|
@ -2401,6 +2402,35 @@ def test_ProxyConfig__add_deployment_resolves_env_refs_on_arbitrary_field(monkey
|
|||
assert deployment.litellm_params.some_future_field == "resolved-custom-value"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stored_drop_params",
|
||||
["true", "os.environ/DROP_PARAMS_FLAG"],
|
||||
)
|
||||
def test_ProxyConfig__add_deployment_turns_stored_drop_params_string_into_bool(monkeypatch, stored_drop_params):
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-1234")
|
||||
monkeypatch.setenv("DROP_PARAMS_FLAG", "true")
|
||||
fake_router = MagicMock()
|
||||
fake_router.upsert_deployment = MagicMock(return_value=True)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router)
|
||||
pc = ProxyConfig()
|
||||
db_model = SimpleNamespace(
|
||||
model_id="model-1",
|
||||
model_name="gpt-5-nano",
|
||||
model_info={"id": "model-1"},
|
||||
litellm_params={
|
||||
"model": encrypt_value_helper(value="openai/gpt-5-nano"),
|
||||
"drop_params": encrypt_value_helper(value=stored_drop_params),
|
||||
},
|
||||
blocked=False,
|
||||
)
|
||||
|
||||
added = pc._add_deployment(db_models=[db_model])
|
||||
deployment = fake_router.upsert_deployment.call_args.kwargs["deployment"]
|
||||
|
||||
assert added == 1
|
||||
assert deployment.litellm_params.drop_params is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ProxyConfig.decrypt_model_list_from_db
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from litellm.types.router import (
|
||||
SPECIAL_MODEL_INFO_PARAMS,
|
||||
Deployment,
|
||||
GenericLiteLLMParams,
|
||||
LiteLLM_Params,
|
||||
ModelInfo,
|
||||
)
|
||||
|
|
@ -89,3 +91,24 @@ def test_pricing_strings_are_coerced_to_float():
|
|||
def test_invalid_pricing_is_rejected():
|
||||
with pytest.raises(ValueError, match='validation error for ModelInfo'):
|
||||
ModelInfo(id="x", input_cost_per_token="free")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
(True, True),
|
||||
("true", True),
|
||||
(" False ", False),
|
||||
("yes", True),
|
||||
(None, None),
|
||||
("os.environ/DROP_PARAMS", "os.environ/DROP_PARAMS"),
|
||||
("v2:gcm:ciphertext-from-a-pre-fix-row", "v2:gcm:ciphertext-from-a-pre-fix-row"),
|
||||
],
|
||||
)
|
||||
def test_drop_params_coerces_flags_and_keeps_unresolved_strings(value, expected):
|
||||
assert GenericLiteLLMParams(drop_params=value).drop_params == expected
|
||||
|
||||
|
||||
def test_drop_params_rejects_non_flag_non_string_values():
|
||||
with pytest.raises(ValidationError):
|
||||
GenericLiteLLMParams(drop_params=2)
|
||||
|
|
|
|||
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -29397,7 +29397,7 @@ export interface components {
|
|||
/** Default Api Key Tpm Limit */
|
||||
default_api_key_tpm_limit?: number | null;
|
||||
/** Drop Params */
|
||||
drop_params?: boolean | null;
|
||||
drop_params?: boolean | string | null;
|
||||
/** Gcs Bucket Name */
|
||||
gcs_bucket_name?: string | null;
|
||||
/** Google Maps Grounding Cost Per Query */
|
||||
|
|
@ -39570,7 +39570,7 @@ export interface components {
|
|||
/** Default Api Key Tpm Limit */
|
||||
default_api_key_tpm_limit?: number | null;
|
||||
/** Drop Params */
|
||||
drop_params?: boolean | null;
|
||||
drop_params?: boolean | string | null;
|
||||
/** Gcs Bucket Name */
|
||||
gcs_bucket_name?: string | null;
|
||||
/** Google Maps Grounding Cost Per Query */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue