Skip to content

fix(otel): normalise list guardrail_mode to tuple in _emit_once to prevent TypeError - #28556

Open
devteamaegis wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
devteamaegis:fix/otel-guardrail-mode-list-unhashable
Open

fix(otel): normalise list guardrail_mode to tuple in _emit_once to prevent TypeError#28556
devteamaegis wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
devteamaegis:fix/otel-guardrail-mode-list-unhashable

Conversation

@devteamaegis

Copy link
Copy Markdown

Problem

When a guardrail is configured with a list-valued mode (e.g. mode: ["pre_call", "post_call"]) and the OpenTelemetry integration is active, every request that triggers the guardrail returns HTTP 500:

TypeError: unhashable type: 'list'

Bisected to #27757 (commit 1c4e4d4).

Closes #28486.

Root cause

_emit_once constructs a deduplification key:

dedupe_key = (self.__class__.__name__, id(self), *scope)

scope is unpacked via *, so if one element is a list (the guardrail_mode), the resulting tuple contains a list. Tuples that contain unhashable elements (like list) are themselves unhashable — so spans_logged.get(dedupe_key) raises TypeError.

Fix

Normalise each scope part before building the key: convert list values to tuple (which is hashable and preserves identity for dedupe purposes):

normalized_scope = tuple(
    tuple(part) if isinstance(part, list) else part
    for part in scope
)
dedupe_key = (self.__class__.__name__, id(self), *normalized_scope)

All non-list scope parts pass through unchanged (strings, ints, floats, None, etc. are all hashable).

Changes

  • litellm/integrations/opentelemetry.py: normalise scope parts in _emit_once
  • tests/test_litellm/integrations/test_opentelemetry.py: add test_guardrail_mode_as_list_does_not_crash

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a TypeError: unhashable type: 'list' crash in _emit_once that occurred whenever a guardrail was configured with a list-valued mode (e.g. ["pre_call", "post_call"]) and the OpenTelemetry integration was active, causing every such request to return HTTP 500.

  • litellm/integrations/opentelemetry.py: Before constructing the deduplication tuple key, each element of scope is normalised — lists are converted to tuples (which are hashable), while all other types pass through unchanged.
  • tests/test_litellm/integrations/test_opentelemetry.py: Adds a mock-only regression test test_guardrail_mode_as_list_does_not_crash that exercises the exact failure path and confirms the fix resolves it.

Confidence Score: 4/5

Safe to merge — the change is a one-line normalisation in a well-contained helper with a direct regression test covering the failure path.

The fix is correct and minimal. The only notable gap is that the new regression test doesn't assert mock_span.end.assert_called_once(), which the other guardrail tests do include, leaving an unchecked span-lifecycle branch.

No files require special attention beyond the minor missing assertion in the new test.

Important Files Changed

Filename Overview
litellm/integrations/opentelemetry.py Normalises list-valued scope parts to tuples in _emit_once so the deduplication key is always hashable; fix is minimal and correctly targeted at the root cause.
tests/test_litellm/integrations/test_opentelemetry.py Adds a focused regression test for the list-mode crash; uses mocks only (no network calls), but omits mock_span.end.assert_called_once() that other guardrail tests include.

Reviews (1): Last reviewed commit: "test(otel): add regression for list guar..." | Re-trigger Greptile

Comment on lines +118 to +120
# Must not raise TypeError: unhashable type: 'list'
otel._create_guardrail_span(kwargs=kwargs, context=None)
otel.tracer.start_span.assert_called_once()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The new test verifies that start_span is called but doesn't assert that the span was properly ended. The sibling test test_create_guardrail_span_with_valid_info includes mock_span.end.assert_called_once(). Without that assertion here, a future regression where the span is created but never closed would go undetected by this test.

Suggested change
# Must not raise TypeError: unhashable type: 'list'
otel._create_guardrail_span(kwargs=kwargs, context=None)
otel.tracer.start_span.assert_called_once()
# Must not raise TypeError: unhashable type: 'list'
otel._create_guardrail_span(kwargs=kwargs, context=None)
otel.tracer.start_span.assert_called_once()
mock_span.end.assert_called_once()

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 3/5

Why blocked:

  • 1 PR-related CI failure (lint) (pr_related_failures, -2 pts)

Details: Score docked for: 1 PR-related CI failure (lint).

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

@github-actions github-actions Bot added the stale label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: OpenTelemetry integration crashes with "unhashable type: 'list'" when guardrail mode is a list

2 participants