From bbf664c6a1442ad66729893ff91023b0b651f1b8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:24:34 +0000 Subject: [PATCH] fix(otel): bound _prune_none recursion depth and allowlist it --- litellm/integrations/opentelemetry.py | 17 +++++++---------- tests/code_coverage_tests/recursive_detector.py | 1 + 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 1f5b9dfa2fe..486950f8735 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -113,19 +113,16 @@ METRIC_METADATA_KEYS: Tuple[str, ...] = ( ) -def _prune_none(value: "AnyValue") -> "AnyValue": - """Recursively drop ``None`` entries from dicts and lists. +_PRUNE_MAX_DEPTH = 16 - The OTLP exporter encodes a log record's body as ``AnyValue``, which has no - representation for ``None`` and raises ``Invalid type NoneType`` on it. That - aborts the export and makes ``BatchLogRecordProcessor`` drop the whole batch, - so a content event carrying a ``None`` (e.g. a ``content=None`` assistant - message) is silently lost. Pruning ``None`` first keeps it (Issue #32996). - """ + +def _prune_none(value: "AnyValue", _depth: int = 0) -> "AnyValue": + if _depth >= _PRUNE_MAX_DEPTH: + return safe_dumps(value) if isinstance(value, dict): - return {key: _prune_none(val) for key, val in value.items() if val is not None} + return {key: _prune_none(val, _depth + 1) for key, val in value.items() if val is not None} if isinstance(value, list): - return [_prune_none(item) for item in value if item is not None] + return [_prune_none(item, _depth + 1) for item in value if item is not None] return value diff --git a/tests/code_coverage_tests/recursive_detector.py b/tests/code_coverage_tests/recursive_detector.py index e08d703d21f..565697ac4f6 100644 --- a/tests/code_coverage_tests/recursive_detector.py +++ b/tests/code_coverage_tests/recursive_detector.py @@ -53,6 +53,7 @@ IGNORE_FUNCTIONS = [ "resolve_oci_schema_anyof", # OCI: bounded by JSON-schema tree depth (no cycles possible in well-formed input). "sanitize_oci_schema", # OCI: bounded by JSON-schema tree depth. "_freeze_for_dedupe", # OTEL: max depth set (default 16, _FREEZE_MAX_DEPTH); fails closed by returning repr(value) at the cap. + "_prune_none", # OTEL: max depth set (default 16, _PRUNE_MAX_DEPTH); fails closed by returning safe_dumps(value) at the cap. "apply_json_merge_patch", # max depth set (_MAX_MERGE_DEPTH=64); fails closed by raising ValueError at the cap. "_filter_mcp_argument_value", # max depth set (DEFAULT_MAX_RECURSE_DEPTH); fails closed by blocking the MCP call at the cap. ]