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
This commit is contained in:
mubashir1osmani 2026-08-24 17:36:45 -04:00
parent a5779151e0
commit 89b67607d9
2 changed files with 59 additions and 32 deletions

View file

@ -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, ...]], ...]:

View file

@ -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)