fix(otel): address CI type/format checks, credential registry, and 100% coverage

This commit is contained in:
Nausherwan Korai 2026-10-27 21:52:56 +00:00
parent 357b2a7fe4
commit cbf9d87ca6
9 changed files with 47 additions and 9 deletions

View file

@ -322,7 +322,7 @@ nothing here imports outside it:
global set with `excluded_services` in their own `callback_vars`; `"none"`
means no datastore spans are excluded, while an omitted value follows the
global set. These destination overrides require OTel v2 and are accepted only
on `langfuse_otel`, `arize`, `weave_otel` and `newrelic` callbacks.
on `langfuse_otel`, `arize`, `weave_otel`, `newrelic` and `mixpanel` callbacks.
- [`baggage.py`](./model/baggage.py) — the single definition of which request-identity
values are promoted into Baggage (so child spans inherit them) and under which
attribute keys.
@ -341,7 +341,7 @@ nothing here imports outside it:
- `genai` — the canonical OpenTelemetry GenAI vocabulary, always present.
- `legacy` — an additional vocabulary using the older semconv-ai / Traceloop
attribute key names, for backends that read those.
- `openinference`, `langfuse`, `weave`, `langtrace` — vendor vocabularies.
- `openinference`, `langfuse`, `weave`, `langtrace`, `mixpanel` — vendor vocabularies.
- `resolve_mappers(names)` turns config names into mapper instances.
### Plumbing (`plumbing/`)

View file

@ -600,6 +600,7 @@ class OpenTelemetryV2(CustomLogger):
trace=call.trace,
session_id=call.session_id,
metadata_keys=tuple(self.config.baggage_metadata_keys),
request_metadata=call.user_id_metadata,
)
end_time_ns: Final = to_ns(end_time)
if carrier is not None and carrier.span is not None:

View file

@ -109,7 +109,11 @@ class RequestIdentity:
)
@classmethod
def from_payload(cls, payload: StandardLoggingPayload) -> RequestIdentity:
def from_payload(
cls,
payload: StandardLoggingPayload,
request_metadata: Mapping[str, str] = MappingProxyType({}),
) -> RequestIdentity:
"""Parse caller identity out of a closed request's payload metadata.
``provider_model`` is resolved here too (see :func:`resolve_provider_model`)
@ -119,7 +123,7 @@ class RequestIdentity:
raw_meta: Final = cast(Mapping[str, object], payload.get("metadata") or {})
payload_view: Final = as_str_mapping(payload)
params: Final = as_str_mapping(payload_view.get("model_parameters")) if payload_view is not None else None
metadata: Final = MappingProxyType(dict(flatten_metadata(raw_meta)))
metadata: Final = MappingProxyType({**request_metadata, **dict(flatten_metadata(raw_meta))})
return cls(
call_id=as_str(payload.get("litellm_call_id")) or as_str(payload.get("id")),
# StandardLoggingMetadata's canonical key is ``user_api_key_team_id``;
@ -201,7 +205,11 @@ class RequestContext:
return self.identity.provider_model
@classmethod
def from_standard_logging_payload(cls, payload: StandardLoggingPayload) -> RequestContext:
def from_standard_logging_payload(
cls,
payload: StandardLoggingPayload,
request_metadata: Mapping[str, str] = MappingProxyType({}),
) -> RequestContext:
raw_meta: Final = cast(Mapping[str, object], payload.get("metadata") or {})
hidden: Final = cast(Mapping[str, object], payload.get("hidden_params") or {})
raw_response: Final = payload.get("response")
@ -216,7 +224,7 @@ class RequestContext:
model_group=model_group,
model_id=as_str(payload.get("model_id")) or _model_info_id(raw_meta.get("model_info")),
api_base=as_str(payload.get("api_base")) or as_str(hidden.get("api_base")),
identity=RequestIdentity.from_payload(payload),
identity=RequestIdentity.from_payload(payload, request_metadata=request_metadata),
)
@ -283,6 +291,7 @@ class LLMCallEvent:
completion_start_seconds: float | None
trace: TraceControls
session_id: str | None
user_id_metadata: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
@classmethod
def from_dict(cls, kwargs: Mapping[str, object]) -> LLMCallEvent:
@ -306,9 +315,21 @@ class LLMCallEvent:
completion_start_seconds=_epoch_seconds(kwargs.get("completion_start_time")),
trace=trace,
session_id=caller_session_id(kwargs, trace),
user_id_metadata=caller_user_id_metadata(kwargs),
)
def caller_user_id_metadata(kwargs: Mapping[str, object]) -> Mapping[str, str]:
params: Final = as_str_mapping(kwargs.get("litellm_params"))
if params is None:
return MappingProxyType({})
bodies: Final = (
*(flatten_metadata(meta) if (meta := as_str_mapping(params.get("metadata"))) is not None else ()),
*(flatten_metadata(lm_meta) if (lm_meta := as_str_mapping(params.get("litellm_metadata"))) is not None else ()),
)
return MappingProxyType({key: value for key, value in bodies if key in _USER_ID_METADATA_KEYS})
def _epoch_seconds(value: object) -> float | None:
return to_seconds(value) if isinstance(value, (datetime, float, int, str)) and not isinstance(value, bool) else None

View file

@ -458,13 +458,14 @@ class LLMCallSpanData:
session_id: str | None = None,
*,
metadata_keys: tuple[str, ...] = (),
request_metadata: Mapping[str, str] = MappingProxyType({}),
) -> LLMCallSpanData:
params: Final = cast(Mapping[str, object], payload.get("model_parameters") or {})
cache_hit: Final = as_bool(payload.get("cache_hit")) is True # pyright: ignore[reportUnknownMemberType] # TypedDict .get on a str key is partially unknown
# The single parse of the request's metadata — the request-vs-provider
# model split, the response model, api base, and identity all come from
# here rather than being re-derived from the raw payload dicts.
context: Final = RequestContext.from_standard_logging_payload(payload)
context: Final = RequestContext.from_standard_logging_payload(payload, request_metadata=request_metadata)
metadata: Final = payload.get("metadata")
decision: Final = metadata["routing_decision"] if metadata and "routing_decision" in metadata else None
# Normalize ``response`` to a dict once so the content/id reads below are a

View file

@ -1922,7 +1922,7 @@ def get_end_user_id_from_request_body(
# Check 5: 'metadata.user_id' or 'metadata.user.id' in request_body (another common pattern)
metadata_dict: Final = _as_dict(request_body.get("metadata"))
user_id_str = _coerce_user_id_to_str(metadata_dict.get("user_id")) or _coerce_user_id_to_str(
metadata_dict.get("user.id")
metadata_dict.get("user.id") # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType] # _as_dict returns untyped dict
)
if user_id_str:
return user_id_str

View file

@ -856,3 +856,14 @@ def test_mixpanel_mapper_stamps_user_id_and_omits_when_absent():
)
assert MixpanelMapper().map(mcp_span) == {"user.id": "mcp_user"}
assert MixpanelMapper().map(_llm_call()) == {}
from litellm.integrations.otel.model.metadata import LLMCallEvent
sdk_event = LLMCallEvent.from_dict(
{
"litellm_params": {"metadata": {"user.id": "sdk_user_789", "ignored_key": "x"}},
"standard_logging_object": {"id": "c3", "model": "gpt-4o", "metadata": {}},
}
)
assert sdk_event.user_id_metadata == {"user.id": "sdk_user_789"}
assert LLMCallEvent.from_dict({}).user_id_metadata == {}

View file

@ -542,6 +542,7 @@ class TestMixpanelCallbackValidation:
with pytest.raises(HTTPException) as exc:
raise_on_invalid_key_logging_config(self._key_metadata({"mixpanel_region": "eu"}))
assert "requires mixpanel_project_token" in str(exc.value.detail)
_validate_team_callback(self._team_data({}))
_validate_team_callback(self._team_data({"mixpanel_project_token": "tok", "mixpanel_region": "in"}))
raise_on_invalid_key_logging_config(
self._key_metadata({"mixpanel_project_token": "tok", "mixpanel_region": "EU"})

View file

@ -102,6 +102,8 @@ CALLBACK_PARAM_CLASSIFICATION: Final[Mapping[str, Classification]] = MappingProx
"newrelic_region": NotSecret("region name"),
"signoz_ingestion_key": Unplanted(),
"signoz_ingestion_endpoint": NotSecret("sink endpoint URL"),
"mixpanel_project_token": Unplanted(),
"mixpanel_region": NotSecret("region name"),
"turn_off_message_logging": NotSecret("boolean logging switch"),
"litellm_disabled_callbacks": NotSecret("list of callback names"),
}

View file

@ -100,7 +100,8 @@ export const CALLBACK_CONFIGS: CallbackConfig[] = [
mixpanel_project_token: "password",
mixpanel_region: "text",
},
description: "Mixpanel Agent Intelligence OpenTelemetry Integration. Setup: https://docs.mixpanel.com/docs/agent-intelligence",
description:
"Mixpanel Agent Intelligence OpenTelemetry Integration. Setup: https://docs.mixpanel.com/docs/agent-intelligence",
},
{
id: "newrelic",