fix(opentelemetry): normalize list-valued guardrail_mode for span dedupe (#28486) - #28500
Conversation
…upe (BerriAI#28486) `_emit_once` builds the per-request dedupe key as `(self.__class__.__name__, id(self), *scope)` and looks it up in a dict. When the guardrail is configured with a list-valued mode (e.g. `mode: ["pre_call", "post_call"]`), `_create_guardrail_span` passes that list straight into `scope`, so the resulting tuple is unhashable. `dict.get` then raises `TypeError: unhashable type: 'list'` and the proxy returns HTTP/500 for every guardrailed request when OTel is enabled. Normalize `guardrail_mode` to a tuple at the call site so the hashable-scope contract documented on `_emit_once` is honored, without changing what is emitted as the span attribute. Regression test exercises the failing code path; it raised before this change and passes after. Refs BerriAI#28486. Bisected to 1c4e4d4 ("Fix 3 OpenTelemetry tracing bugs in proxy integration (BerriAI#27757)").
|
Anai-Guo seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
|
🤖 litellm-agent: This PR is currently BLOCKED from merge. Score: 2/5 ❌ Why blocked:
Details: Score docked for: 1 PR-related CI failure (Greptile gate: score not yet reviewed below required 4/5 — request a Greptile review ( Fix the issues above and push an update — the bot will re-review automatically.
|
|
@greptileai please review — flagged as missing Confidence Score by litellm-agent. |
Greptile SummaryThis PR fixes a
Confidence Score: 3/5The hashability fix is correct and minimal, but the bundled guardrail_response serialization change alters the format of an existing span attribute for string values, which may break downstream trace consumers. The core hashability fix is well-scoped and backed by a direct regression test. However, the same commit also changes how guardrail_response is written to spans: string values previously landed as bare strings and now land as JSON-encoded strings with surrounding quotes. Any monitoring pipeline, dashboard, or parsing code that currently reads guardrail_response and expects a raw string will silently receive a differently-formatted value after this merges. The updated test assertion confirms the change is intentional, but it is not called out in the PR description and will affect existing deployments without warning. litellm/integrations/opentelemetry.py lines 1621-1625 — the guardrail_response serialization change that is bundled with the hashability fix.
|
| Filename | Overview |
|---|---|
| litellm/integrations/opentelemetry.py | Fixes list-valued guardrail_mode hashability crash; also changes guardrail_response serialization from raw-string to json.dumps output, which is a backwards-incompatible behavioral change for string-valued responses. |
| tests/test_litellm/integrations/test_opentelemetry.py | Adds regression test for list-mode hashability fix and two new tests for guardrail_response serialization; updates existing string-response assertion to safe_dumps, confirming the behavioral change. |
Reviews (1): Last reviewed commit: "fix(opentelemetry): normalize list-value..." | Re-trigger Greptile
| guardrail_response = guardrail_information.get("guardrail_response") | ||
| if guardrail_response is not None: | ||
| guardrail_span.set_attribute( | ||
| "guardrail_response", safe_dumps(guardrail_response) | ||
| ) |
There was a problem hiding this comment.
String
guardrail_response is now double-encoded as JSON
The old safe_set_attribute path returned a raw string value for str inputs (via _cast_as_primitive_value_type). The new path calls safe_dumps, which always passes values through json.dumps — so a string like "filtered_content" now becomes '"filtered_content"' (JSON-encoded with surrounding quotes) in the span. Any downstream consumer or dashboard that reads guardrail_response and expects a raw string will see extra surrounding quotes after this change. Dict/complex responses are correctly improved, but existing string-valued responses regress. The updated test reflects this by swapping the assertion to safe_dumps("filtered_content"), which confirms the behavioral change.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The previous reassignment of guardrail_mode to a tuple tripped mypy because the variable inferred type from .get() is GuardrailEventHooks | list[GuardrailEventHooks] | GuardrailMode | None, which does not include tuple. Use a separately-named Any-typed binding for the hashable form passed to _emit_once, preserving the original runtime fix and keeping mypy green.
|
Superseded by #31262 (merged 2026-06-25), which makes the guardrail_mode scope hashable in |
Summary
Closes the HTTP/500 path reported in #28486. When a guardrail is configured with a list-valued
mode(e.g.mode: ["pre_call", "post_call"]) and the OpenTelemetry integration is active, every guardrailed request crashes withTypeError: unhashable type: 'list'.Root cause
OpenTelemetry._emit_oncebuilds the per-request dedupe key as a tuple:_create_guardrail_spanpassesguardrail_information.get("guardrail_mode")into*scopedirectly. When the configured mode is a list, the resulting tuple contains a list, becomes unhashable, andspans_logged.get(dedupe_key)raises._emit_once's docstring already says “scope parts can be any hashable identity” — so the fix belongs at the call site that knows guardrail_mode can be a user-supplied list.Bisected to 1c4e4d4 (#27757), which introduced the
_emit_once-based dedupe loop.Fix
Normalize
guardrail_modeto a tuple before passing it into_emit_once. The value emitted as theguardrail_modespan attribute is unchanged — only the dedupe-key form is normalized.Test plan
TestOpenTelemetryGuardrails.test_create_guardrail_span_with_list_mode_is_hashable— exercises_create_guardrail_spanwithguardrail_mode: ["pre_call", "post_call"]. Without the fix it raisesTypeError: unhashable type: 'list'; with the fix it produces exactly one span.TestOpenTelemetryGuardrailscases (stringguardrail_mode) are unchanged.AI-assisted, human reviewed.