diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index e7e4b16e866..8a36b45707e 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -146,7 +146,7 @@ class LangfuseOtelLogger(OpenTelemetry): ) if not isinstance(request_metadata, dict): return - observation_metadata: Final = { + observation_metadata: Final = { # mutable-ok: stays a real dict for safe_dumps and .get reads below key: value for key, value in log_requester_metadata(redact_user_api_key_info(metadata=request_metadata)).items() if value is not None diff --git a/litellm/integrations/otel/mappers/langfuse.py b/litellm/integrations/otel/mappers/langfuse.py index 40adf2363d9..a5857a7b856 100644 --- a/litellm/integrations/otel/mappers/langfuse.py +++ b/litellm/integrations/otel/mappers/langfuse.py @@ -12,6 +12,7 @@ the JSON-serialized payloads. ``trace_attributes`` maps the caller's trace contr import json from collections.abc import Callable +from types import MappingProxyType from typing import Final from litellm.integrations.otel.mappers.base import AttributeMap, AttrValue, SpanData @@ -60,7 +61,9 @@ class LangfuseMapper: "langfuse.observation.id": lambda d: d.identity.call_id or None, "langfuse.trace.metadata.team_id": lambda d: d.identity.team_id or None, "langfuse.trace.metadata.team_alias": lambda d: d.identity.team_alias or None, - **{f"{LANGFUSE_TRACE_METADATA_PREFIX}{name}": _identity_field(name) for name in TRACE_IDENTITY_FIELDS}, + **MappingProxyType( + {f"{LANGFUSE_TRACE_METADATA_PREFIX}{name}": _identity_field(name) for name in TRACE_IDENTITY_FIELDS} + ), } # Sub-tables folded into their respective JSON blobs. @@ -80,7 +83,13 @@ class LangfuseMapper: # JSON-payload attributes: each builder returns the serialized blob or None. _BLOB_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = { - LANGFUSE_OBSERVATION_METADATA: lambda d: safe_dumps(dict(d.request_metadata)) if d.request_metadata else None, + LANGFUSE_OBSERVATION_METADATA: lambda d: ( + safe_dumps( # mutable-ok: safe_dumps only serializes real dicts + dict(d.request_metadata) + ) + if d.request_metadata + else None + ), "langfuse.observation.model.parameters": lambda d: json_if( collect(LangfuseMapper._MODEL_PARAMS, d.request_params) ), diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 8dc7b2f858a..b8558c743d8 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -423,11 +423,16 @@ def exported_request_metadata(payload: StandardLoggingPayload) -> Mapping[str, o """The request metadata as the logging callbacks export it: ``user_api_key_*`` dropped when ``litellm.redact_user_api_key_info`` is on, header keys nested under ``requester_metadata``, ``None`` values dropped.""" - raw_meta: Final = cast(Mapping[str, object], payload.get("metadata") or {}) - redacted: Final[Mapping[str, object]] = cast( - Mapping[str, object], redact_user_api_key_info(metadata=dict(raw_meta)) + raw_meta: Final = cast( # cast-ok: StandardLoggingPayload.get returns Any | None + Mapping[str, object], payload.get("metadata") or MappingProxyType({}) + ) + redacted: Final = cast( # cast-ok: redact_user_api_key_info is untyped + Mapping[str, object], + redact_user_api_key_info(metadata=dict(raw_meta)), # mutable-ok: the redactor requires a real dict + ) + exported: Final = cast( # cast-ok: log_requester_metadata is untyped + Mapping[str, object], log_requester_metadata(redacted) ) - exported: Final[Mapping[str, object]] = cast(Mapping[str, object], log_requester_metadata(redacted)) return MappingProxyType({key: value for key, value in exported.items() if value is not None}) diff --git a/litellm/integrations/otel/model/payloads.py b/litellm/integrations/otel/model/payloads.py index c1d421b5d52..3fda279f732 100644 --- a/litellm/integrations/otel/model/payloads.py +++ b/litellm/integrations/otel/model/payloads.py @@ -388,7 +388,7 @@ class LLMCallSpanData: response_cost: float | None server: ServerInfo | None identity: RequestIdentity - request_metadata: Mapping[str, object] = field(default_factory=lambda: cast(Mapping[str, object], {})) + request_metadata: Mapping[str, object] = field(default_factory=dict) is_streaming: bool | None = None cost: LLMCost = field(default_factory=LLMCost) tools: tuple[ToolDefinition, ...] = ()