diff --git a/litellm/router.py b/litellm/router.py index c7cff4ba412..8636f6684a9 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -1518,6 +1518,11 @@ class Router: if "metadata" not in silent_kwargs: silent_kwargs["metadata"] = {} + # OTel spans are not safe to use across event loops. The silent + # experiment runs in a new event loop, so strip the span to prevent + # cross-loop tracing races or span corruption. + silent_kwargs["metadata"].pop("litellm_parent_otel_span", None) + silent_kwargs["metadata"]["is_silent_experiment"] = True # Force stream=False so the response is fully consumed and callbacks fire diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index 6be95ca91aa..24f574a4f8b 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -73,7 +73,11 @@ def test_get_silent_experiment_kwargs(): # so that setting model_group / is_silent_experiment on the silent dict # doesn't corrupt the primary call's metadata. assert result["metadata"] is not kwargs["metadata"] - # Original metadata must NOT be mutated + # OTel span must be stripped from the silent copy — it's not safe to use + # across event loops (silent experiment runs in a new event loop). + assert "litellm_parent_otel_span" not in result["metadata"] + # Original metadata must NOT be mutated — must carry the real span, + # not safe_deep_copy's temporary "placeholder" string. assert "is_silent_experiment" not in kwargs["metadata"] assert kwargs["metadata"]["litellm_parent_otel_span"] is mock_span assert kwargs["metadata"]["user_api_key_auth"] is mock_auth