fix(otel v2): satisfy type discipline gate for metadata export

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-23 19:39:31 +00:00
parent d964f34c06
commit ad56bcdfed
4 changed files with 22 additions and 8 deletions

View file

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

View file

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

View file

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

View file

@ -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, ...] = ()