From c071e01fb6154fc7d240d2df5893e9a4d33abbca Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 21 Feb 2026 10:48:10 -0800 Subject: [PATCH] fix: address PR review comments for model_dump_with_preserved_fields - Restore preserve_fields param for backward compatibility (deprecated) - Use zip() instead of index-based iteration to prevent IndexError - Add backward compatibility test --- litellm/proxy/utils.py | 9 ++--- .../test_model_dump_with_preserved_fields.py | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 3e9990e022..0ad081a107 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -4565,9 +4565,6 @@ def validate_model_access( ) -# Fields within each choice that must be preserved as null (not stripped) -# for OpenAI API compatibility. Each tuple is (sub_object, field_name). -# To add a new preserved field, just append a tuple here. _PRESERVED_NONE_FIELDS: List[tuple] = [ ("message", "content"), # null when tool_calls present (issue #6677) ("message", "role"), # always required by OpenAI spec @@ -4577,6 +4574,7 @@ _PRESERVED_NONE_FIELDS: List[tuple] = [ def model_dump_with_preserved_fields( obj: Any, + preserve_fields: Optional[List[str]] = None, exclude_unset: bool = True, ) -> Dict[str, Any]: """ @@ -4588,6 +4586,7 @@ def model_dump_with_preserved_fields( Args: obj: The Pydantic BaseModel instance to serialize + preserve_fields: Deprecated, kept for backward compatibility. exclude_unset: Whether to exclude fields that were not explicitly set Returns: @@ -4600,9 +4599,7 @@ def model_dump_with_preserved_fields( return result obj_choices = obj.choices - for i, choice_dict in enumerate(choices): - choice_obj = obj_choices[i] - + for choice_obj, choice_dict in zip(obj_choices, choices): for sub_object, field_name in _PRESERVED_NONE_FIELDS: sub_dict = choice_dict.get(sub_object) if sub_dict is None: diff --git a/tests/test_litellm/proxy/test_model_dump_with_preserved_fields.py b/tests/test_litellm/proxy/test_model_dump_with_preserved_fields.py index 5ce2c6cba5..3001c87ebe 100644 --- a/tests/test_litellm/proxy/test_model_dump_with_preserved_fields.py +++ b/tests/test_litellm/proxy/test_model_dump_with_preserved_fields.py @@ -375,3 +375,39 @@ def test_delta_dynamic_attributes_in_model_dump(): delta_no_role = Delta(content=None, role=None) dump_no_role = delta_no_role.model_dump(exclude_none=True) assert "role" not in dump_no_role + + +def test_preserve_fields_param_backward_compat(): + """preserve_fields parameter is accepted (deprecated) without error.""" + response = ModelResponse( + choices=[ + Choices( + finish_reason="tool_calls", + index=0, + message=Message( + content=None, + role="assistant", + tool_calls=[ + { + "id": "call_1", + "type": "function", + "function": {"name": "f", "arguments": "{}"}, + } + ], + ), + ) + ], + ) + result_default = model_dump_with_preserved_fields(response, exclude_unset=True) + result_explicit = model_dump_with_preserved_fields( + response, + preserve_fields=[ + "choices.*.message.content", + "choices.*.message.role", + "choices.*.delta.content", + ], + exclude_unset=True, + ) + assert result_default == result_explicit + assert result_default["choices"][0]["message"]["content"] is None + assert result_default["choices"][0]["message"]["role"] == "assistant"