From c725772d44cc35dfefc9f6e34156a292e73c8ae8 Mon Sep 17 00:00:00 2001 From: bhumikadangayach Date: Wed, 28 Oct 2026 12:41:45 +0530 Subject: [PATCH 1/2] fix(otel): don't crash set_attributes on responses without .get() --- litellm/integrations/opentelemetry.py | 5 +++ tests/unit/integrations/test_opentelemetry.py | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 0d0e309034a..04eb76021a9 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -2578,6 +2578,11 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): value=optional_params.get("user"), ) + # Some responses (MCP CallToolResult, DecisionsResponse, list) have no + # .get(). They carry no LLM response fields, so treat them as None. + if not callable(getattr(response_obj, "get", None)): + response_obj = None + # The unique identifier for the LLM call. # Completions have a provider response ID (e.g. "chatcmpl-xxx"), # but Embeddings and Image-gen responses do not. Fall back to diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py index f908c685382..eb1d3d00bb7 100644 --- a/tests/unit/integrations/test_opentelemetry.py +++ b/tests/unit/integrations/test_opentelemetry.py @@ -4185,6 +4185,38 @@ class TestResponseIdFallback(unittest.TestCase): mock_span.set_attribute.assert_any_call("litellm.call_id", call_id) + def test_litellm_call_id_set_when_response_obj_has_no_get(self): + """Responses without .get() (MCP CallToolResult, DecisionsResponse, list) + must not abort set_attributes before litellm.call_id is set.""" + from pydantic import BaseModel + + class NoGetResponse(BaseModel): + id: str = "dec-1" + + call_id = "my-litellm-call-uuid-789" + for response_obj in (NoGetResponse(), ["tool output"]): + with self.subTest(response_type=type(response_obj).__name__): + otel = OpenTelemetry() + mock_span = MagicMock() + kwargs = { + "model": "my-model", + "optional_params": {}, + "litellm_params": {}, + "standard_logging_object": { + "id": "litellm-call-id-1", + "litellm_call_id": call_id, + "call_type": "call_mcp_tool", + "metadata": {}, + }, + } + + otel.set_attributes(mock_span, kwargs, response_obj) + + mock_span.set_attribute.assert_any_call("litellm.call_id", call_id) + mock_span.set_attribute.assert_any_call( + "gen_ai.response.id", "litellm-call-id-1" + ) + class TestOpenTelemetryResponsesAPI(unittest.TestCase): """ From 05f9e0afa9454525a87e5bab6108aa3120ea3447 Mon Sep 17 00:00:00 2001 From: bhumikadangayach Date: Wed, 28 Oct 2026 13:13:31 +0530 Subject: [PATCH 2/2] chore(otel): add rebind-ok reason for type discipline gate --- litellm/integrations/opentelemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 04eb76021a9..3a0d10ffbd3 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -2581,7 +2581,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): # Some responses (MCP CallToolResult, DecisionsResponse, list) have no # .get(). They carry no LLM response fields, so treat them as None. if not callable(getattr(response_obj, "get", None)): - response_obj = None + response_obj = None # rebind-ok: no .get() means no LLM response fields # The unique identifier for the LLM call. # Completions have a provider response ID (e.g. "chatcmpl-xxx"),