From bf8c35e2d4f6a74759bc5c9cd3d8c0c9020b376e Mon Sep 17 00:00:00 2001 From: Yashwanth-Kumar-Kotla Date: Mon, 31 Aug 2026 15:40:52 -0500 Subject: [PATCH] fix(otel): don't cast None metadata values to empty-string span attributes user_api_key_budget_reset_at (and any other metadata field that's sometimes None, e.g. a key with no budget configured) was being cast to "" by _cast_as_primitive_value_type and set on the span like any other value. A field that sends two different shapes for the same attribute -- an ISO timestamp string sometimes, an empty string other times -- breaks backends that infer a field's type from the first value they see, such as OpenSearch's dynamic index mapping. Skip None values in the metadata attribute loop instead, matching the same pattern already used for cost_breakdown two loops below. Fixes #29583 --- litellm/integrations/opentelemetry.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index e8f3b305139..dc0cef16d66 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -2361,7 +2361,15 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): ############################################# metadata: Final = standard_logging_payload["metadata"] for key, value in metadata.items(): - self.safe_set_attribute(span=span, key=f"metadata.{key}", value=value) + # Skip None values instead of letting safe_set_attribute cast them to "". + # Fields that are sometimes a real value (e.g. an ISO timestamp string + # like user_api_key_budget_reset_at) and sometimes None would otherwise + # send two different shapes for the same attribute, and backends that + # infer a field's type from the first value they see (e.g. OpenSearch's + # dynamic mapping) lock onto whichever shape arrives first -- causing the + # other shape to fail once the mapping is set. See issue #29583. + if value is not None: + self.safe_set_attribute(span=span, key=f"metadata.{key}", value=value) # get hidden params hidden_params: Final = getattr(standard_logging_payload, "hidden_params", None) or (