mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-24 05:45:11 +00:00
fix(otel): make type-discipline suppressions survive ruff formatting
- Keep mutable-ok / cast-ok suppressions on the same line as the annotated or cast expression (shorten reasons to stay under the 120-col ruff limit) so LIT013 does not report them as dead. - Resolve the Responses-API content loop via cast() to a Sequence[object] variable instead of an inline conditional, which surfaced a reportUnknownArgumentType on the merge-base delta. - Return type stays list[...] (mutable-ok) to match the OTELGenAISemconvMixin base signature; only the parameter widens to Sequence. - type-discipline gate: no LIT rule grew past merge-base; basedpyright per-rule deltas are all negative. Signed-off-by: apex-mochen <2756823972@qq.com>
This commit is contained in:
parent
fdaa93afd5
commit
a8f6b07eb5
2 changed files with 27 additions and 24 deletions
|
|
@ -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", ""),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue