From 89b67607d935bb22f7a2c92c7027c0f69b464832 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Mon, 24 Aug 2026 17:36:45 -0400 Subject: [PATCH] fix(ci): flatten multipart form fields without recursion Walk nested form values with a depth-capped stack so the recursive-function detector stops failing after the internal staging merge --- .../litellm_core_utils/llm_request_utils.py | 77 ++++++++++++------- .../test_llm_request_utils.py | 14 +++- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/litellm/litellm_core_utils/llm_request_utils.py b/litellm/litellm_core_utils/llm_request_utils.py index c833d57b6a9..c6bf798d179 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from typing import Final import litellm +from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH def _form_field_value(value: object) -> str: @@ -13,18 +14,27 @@ def _form_field_value(value: object) -> str: def _flatten_form_field(key: str, value: object) -> tuple[tuple[str, str], ...]: - if isinstance(value, Mapping): - return tuple( - item for subkey, subvalue in value.items() for item in _flatten_form_field(f"{key}[{subkey}]", subvalue) - ) - if isinstance(value, (list, tuple)): - return tuple(item for entry in value for item in _flatten_form_field(f"{key}[]", entry)) - if value is None: - return () - serialized: Final = _form_field_value(value) - if not serialized: - return () - return ((key, serialized),) + work: Final[list[tuple[str, object, int]]] = [(key, value, 0)] # mutable-ok: depth-capped stack, avoids recursion + out: Final[list[tuple[str, str]]] = [] # mutable-ok: local accumulator + while work: + current_key, current_value, depth = work.pop() + if depth > DEFAULT_MAX_RECURSE_DEPTH: + raise ValueError("form field nesting exceeds max depth") + if isinstance(current_value, Mapping): + work.extend( + (f"{current_key}[{subkey}]", subvalue, depth + 1) + for subkey, subvalue in reversed(tuple(current_value.items())) + ) + continue + if isinstance(current_value, (list, tuple)): + work.extend((f"{current_key}[]", entry, depth + 1) for entry in reversed(tuple(current_value))) + continue + if current_value is None: + continue + serialized = _form_field_value(current_value) + if serialized: + out.append((current_key, serialized)) + return tuple(out) def _is_form_scalar(value: object) -> bool: @@ -32,23 +42,32 @@ def _is_form_scalar(value: object) -> bool: def _flatten_form_data_field(key: str, value: object) -> tuple[tuple[str, str | tuple[str, ...]], ...]: - if isinstance(value, Mapping): - return tuple( - item - for subkey, subvalue in value.items() - for item in _flatten_form_data_field(f"{key}[{subkey}]", subvalue) - ) - if isinstance(value, (list, tuple)): - if all(_is_form_scalar(entry) for entry in value): - serialized_fields: Final = tuple(field for entry in value if (field := _form_field_value(entry))) - return ((key, serialized_fields),) if serialized_fields else () - return tuple(item for entry in value for item in _flatten_form_data_field(f"{key}[]", entry)) - if value is None: - return () - serialized: Final = _form_field_value(value) - if not serialized: - return () - return ((key, serialized),) + work: Final[list[tuple[str, object, int]]] = [(key, value, 0)] # mutable-ok: depth-capped stack, avoids recursion + out: Final[list[tuple[str, str | tuple[str, ...]]]] = [] # mutable-ok: local accumulator + while work: + current_key, current_value, depth = work.pop() + if depth > DEFAULT_MAX_RECURSE_DEPTH: + raise ValueError("form field nesting exceeds max depth") + if isinstance(current_value, Mapping): + work.extend( + (f"{current_key}[{subkey}]", subvalue, depth + 1) + for subkey, subvalue in reversed(tuple(current_value.items())) + ) + continue + if isinstance(current_value, (list, tuple)): + if all(_is_form_scalar(entry) for entry in current_value): + serialized_fields = tuple(field for entry in current_value if (field := _form_field_value(entry))) + if serialized_fields: + out.append((current_key, serialized_fields)) + continue + work.extend((f"{current_key}[]", entry, depth + 1) for entry in reversed(tuple(current_value))) + continue + if current_value is None: + continue + serialized = _form_field_value(current_value) + if serialized: + out.append((current_key, serialized)) + return tuple(out) def flatten_form_field_values(*sources: Mapping[str, object] | None) -> tuple[tuple[str, str | tuple[str, ...]], ...]: diff --git a/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py b/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py index 3a09702de45..765c47547ce 100644 --- a/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py @@ -1,4 +1,5 @@ import httpx +import pytest from litellm.litellm_core_utils.llm_request_utils import ( flatten_form_field_values, @@ -80,9 +81,7 @@ def test_flatten_form_field_values_later_source_wins_on_collision(): def test_flatten_form_field_values_keeps_scalar_lists_as_repeated_fields(): - assert flatten_form_field_values( - {"loras": ["a", "b", "c"], "generation_config": {"tags": [1, 2]}, "seed": 42} - ) == ( + assert flatten_form_field_values({"loras": ["a", "b", "c"], "generation_config": {"tags": [1, 2]}, "seed": 42}) == ( ("loras", ("a", "b", "c")), ("generation_config[tags]", ("1", "2")), ("seed", "42"), @@ -97,3 +96,12 @@ def test_flatten_form_field_values_scalar_list_survives_update_into_multipart(): assert names.count("loras") == 2 assert names.count("model") == 1 + + +def test_flatten_form_field_values_rejects_over_deep_nesting(): + nested: object = "leaf" + for _ in range(102): + nested = {"k": nested} + assert isinstance(nested, dict) + with pytest.raises(ValueError, match="max depth"): + flatten_form_field_values(nested)