diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index d15b1c6a5bd..1533a88cf74 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -1,7 +1,7 @@ import os import threading from collections import OrderedDict -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from datetime import datetime @@ -2768,8 +2768,8 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): span.set_attribute(key, primitive_value) def _transform_messages_to_otel_semantic_conventions( - self, messages: list[dict[str, object]] | str - ) -> list[dict[str, object]]: + self, messages: Sequence[dict[str, object]] | str + ) -> list[dict[str, object]]: # mutable-ok: match mixin base return """ Transforms LiteLLM/OpenAI style messages into OTEL GenAI 1.38 compliant format. OTEL expects a 'parts' array instead of a single 'content' string. @@ -2783,7 +2783,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): } ] - transformed: Final[list[dict[str, object]]] = [] + transformed: Final = [] for msg in messages: # Normalize pydantic / LiteLLM wrapper objects (e.g. a ModelResponse # Message) to something we can read with .get(); "tool_calls" in msg @@ -2791,17 +2791,17 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): normalized = self._to_dict(msg) if normalized is None: continue - msg = cast(dict[str, object], normalized) + msg = cast(dict[str, object], normalized) # cast-ok: _to_dict guarantees a dict-like payload role = msg.get("role", "user") content = msg.get("content", "") - parts: list[dict[str, object]] = [] + parts: list[dict[str, object]] = [] # mutable-ok: local accumulator returned read-only if isinstance(content, str): parts.append({"type": "text", "content": content}) elif isinstance(content, list): # Handle multi-modal content if necessary - for part in cast(list[object], content): + for part in cast(list[object], content): # cast-ok: content already narrowed to list if isinstance(part, dict): parts.append(part) else: @@ -2810,14 +2810,14 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): # Record tool calls as GenAI tool_call parts (OpenLLMetry-compatible) # so id / name / arguments are not dropped from the trace. raw_tool_calls = msg.get("tool_calls") - tool_calls: list[object] = raw_tool_calls if isinstance(raw_tool_calls, list) else [] + tool_calls: Sequence[object] = raw_tool_calls if isinstance(raw_tool_calls, list) else () for tc in tool_calls: if isinstance(tc, dict): - tc_dict = cast(dict[str, object], tc) + tc_dict = cast(dict[str, object], tc) # cast-ok: tc narrowed to dict tc_id = tc_dict.get("id") fn = tc_dict.get("function") if isinstance(fn, dict): - fn_dict = cast(dict[str, object], fn) + fn_dict = cast(dict[str, object], fn) # cast-ok: fn narrowed to dict fn_name = fn_dict.get("name") fn_arguments = fn_dict.get("arguments") else: @@ -2827,17 +2827,17 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): # pydantic tool-call objects (e.g. ChatCompletionMessageToolCall): # read fields via literal getattr; arbitrary objects degrade to # None exactly like the old .get()/getattr pair. - tc_id = cast(object, getattr(tc, "id", None)) - fn = cast(object, getattr(tc, "function", None)) + tc_id = cast(object, getattr(tc, "id", None)) # cast-ok: getattr returns object + fn = cast(object, getattr(tc, "function", None)) # cast-ok: getattr returns object if fn is None: fn_name = None fn_arguments = None else: - fn_name = cast(object, getattr(fn, "name", None)) - fn_arguments = cast(object, getattr(fn, "arguments", None)) + fn_name = cast(object, getattr(fn, "name", None)) # cast-ok: getattr returns object + fn_arguments = cast(object, getattr(fn, "arguments", None)) # cast-ok: getattr returns object parts.append({"type": "tool_call", "id": tc_id, "name": fn_name, "arguments": fn_arguments}) - transformed_msg: dict[str, object] = {"role": role, "parts": parts} + transformed_msg: dict[str, object] = {"role": role, "parts": parts} # mutable-ok: key-assigned below if "id" in msg: transformed_msg["id"] = msg["id"] if "tool_call_id" in msg: @@ -2904,21 +2904,24 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): This method converts them to the same ``{"role": ..., "parts": [...]}`` format used by ``_transform_choices_to_otel_semantic_conventions``. """ - transformed: Final[list[dict[str, object]]] = [] + transformed: Final = [] for raw_item in output: item = self._to_dict(raw_item) if item is None: continue - item = cast(dict[str, object], item) + item = cast(dict[str, object], item) # cast-ok: _to_dict guarantees a dict-like payload if item.get("type") == "message": role = item.get("role", "assistant") - parts: list[dict[str, object]] = [] + parts: list[dict[str, object]] = [] # mutable-ok: local accumulator returned read-only raw_content_list = item.get("content", []) - for raw_content in cast(list[object], raw_content_list) if isinstance(raw_content_list, list) else []: + content_items = cast( # cast-ok: narrowed to list above + Sequence[object], raw_content_list if isinstance(raw_content_list, list) else () + ) + for raw_content in content_items: content = self._to_dict(raw_content) if content is None: continue - content = cast(dict[str, object], content) + content = cast(dict[str, object], content) # cast-ok: _to_dict guarantees a dict-like payload if content.get("type") == "output_text": text = content.get("text", "") if text: @@ -2927,7 +2930,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): transformed.append({"role": role, "parts": parts}) elif item.get("type") == "function_call": # Surface tool calls from Responses API output - part: dict[str, object] = { + part: dict[str, object] = { # mutable-ok: assembled then key-assigned before append "type": "tool_call", "name": item.get("name", ""), "arguments": item.get("arguments", ""), diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index 9b961839a5c..d98103e0652 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -389,7 +389,7 @@ class ChunkProcessor: def _read_tool_calls(payload: object) -> object: """Return a delta/message payload's ``tool_calls``, dict or object form.""" if isinstance(payload, dict): - return cast(dict[str, object], payload).get("tool_calls") + return cast(dict[str, object], payload).get("tool_calls") # cast-ok: payload narrowed to dict if isinstance(payload, Delta): return payload.tool_calls return None @@ -457,9 +457,9 @@ class ChunkProcessor: if hasattr(choice, "finish_reason"): # StreamingChoices is a dynamic OpenAIObject: the attribute may # or may not exist at runtime, so read via literal getattr. - chunk_finish_reason = cast(str, getattr(choice, "finish_reason", None)) + chunk_finish_reason = cast(str, getattr(choice, "finish_reason", None)) # cast-ok: dynamic attr elif "finish_reason" in choice: - chunk_finish_reason = cast(str, choice["finish_reason"]) + chunk_finish_reason = cast(str, choice["finish_reason"]) # cast-ok: key checked if chunk_finish_reason is not None: finish_reason = chunk_finish_reason if finish_reason == "stop" and saw_tool_call: