litellm/tests/code_coverage_tests
yucheng-berri a545c493d7
Some checks are pending
LiteLLM Rust / rustfmt, clippy, test (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
fix(otel): hashable scope for _emit_once when guardrail_mode is list (#31262)
* fix(otel): hashable scope for _emit_once when guardrail_mode is list

`_emit_once` keys `spans_logged` by `(class, id, *scope)`. When a
guardrail entry's `guardrail_mode` arrives as a `List[GuardrailEventHooks]`
(the shape Presidio expands to with `output_parse_pii: true`, and the
shape `event_hook` carries for any `mode: [...]` in config), the tuple
contains a list and `spans_logged.get(dedupe_key)` raises
`TypeError: unhashable type: 'list'`. On the post-call path this fires
inside the logging callback and is swallowed; the request returns 200 but
the OTEL `guardrail` span is silently dropped. On the blocking path the
same error surfaces as HTTP 500.

Adds `_freeze_for_dedupe`, a small recursive normalizer that turns lists
and tuples into tuples, sets into frozensets, dicts into frozensets of
`(key, value)` pairs, and falls back to `repr` for arbitrary
unhashables. Applied inside `_emit_once` before the dict lookup, so all
three callsites are protected without touching the guardrail-specific
callsite. Helper assumes acyclic input; `guardrail_mode` values are
built fresh from config (str enums, lists of str enums, TypedDict of
str/list-of-str), so no cycle can arise in practice.

Regression tests in `TestOpenTelemetrySpanDedupe` cover the list crash,
distinct-list-scope collision, dict and set scope parts, and an
end-to-end `_create_guardrail_span` exercise that confirms exactly one
`guardrail` span is emitted across repeated lifecycle entrypoints. Each
new test fails on a reverted helper (4/4 mutation kill)

* fix(otel): cap _freeze_for_dedupe recursion depth and ignore in recursive detector

CI's recursive_detector blocks new recursive functions in litellm/ unless they
are in the allowlist with a documented bound. Cap the helper at 16 levels and
return repr(value) past the cap; this is well past the realistic depth of
guardrail_mode (1-3 levels) and means a future caller passing a cyclic
container can no longer push the proxy logging path into a RecursionError.
Add a regression test that exercises the cycle path.

* refactor(otel): annotate _freeze_for_dedupe return as a HashableScope union

Per review feedback from @mateo-berri: replace the loose `-> object` annotation
with a recursive `HashableScope` union (str | int | float | bool | bytes | None
| Tuple[HashableScope, ...] | FrozenSet[HashableScope]) so the helper's contract
is visible at the signature. Replace the `try/except hash(value); return value`
passthrough with an explicit isinstance check over the hashable-scalar types so
the type checker can narrow without requiring `cast(Hashable, value)` on the
return. Symmetric: dict keys also flow through the freezer (a TypedDict key is
already a string in practice, so behaviorally identical). All 16 regression
tests still pass; mutation kill behavior preserved

* fix: avoid explicit casting

---------

Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
2026-06-25 11:59:35 -07:00
..
azure_client_usage_test.py
ban_constant_numbers.py
ban_copy_deepcopy_kwargs.py
bedrock_pricing.py
callback_manager_test.py
check_data_replace_usage.py
check_endpoint_coverage.py Revert "[Feature] Add /public/supported_endpoints endpoint" 2026-02-26 17:21:43 -08:00
check_fastuuid_usage.py
check_get_model_cost_key_performance.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
check_guardrail_apply_decorator.py content filter test fix 2026-02-12 17:54:16 -08:00
check_licenses.py bump deps (#29208) (#29226) 2026-05-28 16:48:14 -07:00
check_provider_folders_documented.py Revert "[Feature] Add /public/supported_endpoints endpoint" 2026-02-26 17:21:43 -08:00
check_spanattributes_value_usage.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
check_unsafe_enterprise_import.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
code_qa_check_tests.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
enforce_llms_folder_style.py feat(search): add TinyFish as search provider (#30634) 2026-06-18 09:17:53 -07:00
ensure_async_clients_test.py fix code QA check 2025-10-07 17:49:57 -07:00
info_log_check.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
liccheck.ini fix(check_licenses): read PEP 639 license-expression metadata (#28529) 2026-05-22 11:22:38 -07:00
license_cache.json fix(deps): update python-multipart to >=0.0.20 in CI and test configs 2026-03-03 15:10:39 -03:00
litellm_logging_code_coverage.py
log.txt
memory_test.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
pass_through_code_coverage.py
prevent_key_leaks_in_exceptions.py
recursive_detector.py fix(otel): hashable scope for _emit_once when guardrail_mode is list (#31262) 2026-06-25 11:59:35 -07:00
router_code_coverage.py fix(ci): resolve mypy and check_code_and_doc_quality CI failures (#21812) 2026-02-21 13:08:47 -08:00
router_enforce_line_length.py
test_aio_http_image_conversion.py fix img URL for tests 2025-11-22 09:41:15 -08:00
test_ban_set_verbose.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_chat_completion_imports.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_proxy_types_import.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_router_strategy_async.py
user_api_key_auth_code_coverage.py