From 781f824974ced749f90d733af82c40cc60be9a39 Mon Sep 17 00:00:00 2001 From: Mrun25 <165297479+Mrun25@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:29:08 +0530 Subject: [PATCH] fix(proxy): serialize temporal values in model_info on model update update_db_model only isoformatted top-level datetime.datetime values before json.dumps, so a model with saved model_info.access_windows (whose start/end are datetime.time) 500'd on any edit with 'Object of type time is not JSON serializable'. Fixes #46872 --- .../model_management_endpoints.py | 9 ++- .../test_model_management_endpoints.py | 58 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 36071bfe51a..fdd3b06e063 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -1055,6 +1055,12 @@ def _loaded_catalog_entry( return loaded_catalog().get(catalog_key, MappingProxyType({})) +def _json_temporal_default(value: object) -> str: + if isinstance(value, (datetime.datetime, datetime.date, datetime.time)): + return value.isoformat() + raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") + + def update_db_model( db_model: Deployment, updated_patch: updateDeployment, @@ -1154,7 +1160,8 @@ def update_db_model( prisma_compatible_model_dict: Final = PrismaCompatibleUpdateDBModel( model_name=merged_model_name, litellm_params=json.dumps(merged_litellm_params), - model_info=json.dumps(merged_model_info), + # date/time values nest inside model_info (e.g. access_windows start/end) + model_info=json.dumps(merged_model_info, default=_json_temporal_default), ) if updated_patch.blocked is not None: diff --git a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py index beffe454b8d..ca03914de2b 100644 --- a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py @@ -3403,6 +3403,64 @@ class TestUpdateDBModelBlocked: assert "blocked" not in result +class TestJsonTemporalDefault: + def test_raises_type_error_for_unsupported_values(self): + from litellm.proxy.management_endpoints.model_management_endpoints import ( + _json_temporal_default, + ) + + with pytest.raises(TypeError, match="not JSON serializable"): + _json_temporal_default(object()) + + def test_isoformats_date_and_time(self): + import datetime + + from litellm.proxy.management_endpoints.model_management_endpoints import ( + _json_temporal_default, + ) + + assert _json_temporal_default(datetime.time(9, 30)) == "09:30:00" + assert _json_temporal_default(datetime.date(2026, 1, 2)) == "2026-01-02" + + +class TestUpdateDBModelAccessWindows: + """model_info.access_windows holds datetime.time values; editing a model with a + saved window must not 500 on json.dumps (issue #46872).""" + + def test_update_db_model_serializes_access_window_times(self): + import datetime + + from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model + from litellm.router_utils.access_windows import parse_access_windows + from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo, updateDeployment + + db_model = Deployment( + model_name="gpt-4o", + litellm_params=LiteLLM_Params(model="openai/gpt-4o"), + model_info=ModelInfo( + id="dep-window", + access_windows=( + { + "start": datetime.time(9, 0), + "end": datetime.time(17, 0), + "timezone": "America/New_York", + "team_ids": ("team-1",), + }, + ), + ), + ) + + result = update_db_model(db_model=db_model, updated_patch=updateDeployment()) + + stored = json.loads(result["model_info"]) + assert stored["access_windows"][0]["start"] == "09:00:00" + assert stored["access_windows"][0]["end"] == "17:00:00" + # the stored JSON still parses back into valid windows - no data lost + windows = parse_access_windows(stored) + assert len(windows) == 1 + assert windows[0].start == datetime.time(9, 0) + + 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