Skip to content

refactor(types): cut 653 implicit and explicit Any diagnostics across 11 modules - #36054

Merged
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_reduce_any_types
Aug 6, 2026
Merged

refactor(types): cut 653 implicit and explicit Any diagnostics across 11 modules#36054
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_reduce_any_types

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • 25k implicit/explicit Any in basedpyright, ceilings never move
  • untyped payloads hide real bugs from the checker
  • get_marketplace 500s on a NULL manifest_json row

How it solves it:

  • types 11 high-density modules against real shapes
  • removes 653 diagnostics, ratchets all three budgets down
  • guards the nullable column, with a regression test

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The one runtime change in this PR is the manifest_json null guard, so that is what the proof covers. Before is the base commit 388943ac, after is this branch at 4ab7a33d. Both runs hit the same live proxy on localhost:4000 backed by a real Postgres, against the same two database rows, with no mocks

Setup, run once against either commit:

$ python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml

$ curl -sS -X POST http://localhost:4000/claude-code/plugins \
    -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
    -d '{"name":"good-plugin","version":"1.0.0","source":{"source":"git-subdir","url":"https://github.com/org/monorepo.git","path":"plugins/my-plugin"}}'
{"status":"success","action":"created","plugin":{"id":"c262fecf-ddbc-4551-841c-07f9233dde97","name":"good-plugin","version":"1.0.0","description":null,"source":{"source":"git-subdir","url":"https://github.com/org/monorepo.git","path":"plugins/my-plugin"},"enabled":true}}

$ psql -d litellm -c "insert into \"LiteLLM_ClaudeCodePluginTable\" (id, name, enabled, manifest_json) values ('null-mf-1','null-manifest-plugin',true,NULL)"
INSERT 0 1

That second row is what the schema already permits, since schema.prisma:1340 declares manifest_json String?

Before, on 388943ac, one such row takes down the whole endpoint:

$ curl -sS -w "\nHTTP %{http_code}\n" http://localhost:4000/claude-code/marketplace.json
{"detail":{"error":"Failed to generate marketplace: the JSON object must be str, bytes or bytearray, not NoneType"}}
HTTP 500

with this in the proxy log:

claude_code_marketplace.py:126 - Error generating marketplace: the JSON object must be str, bytes or bytearray, not NoneType
TypeError: the JSON object must be str, bytes or bytearray, not NoneType

After, on 4ab7a33d, same rows, same request:

$ curl -sS -w "\nHTTP %{http_code}\n" http://localhost:4000/claude-code/marketplace.json
{"name":"litellm","owner":{"name":"LiteLLM","email":"support@litellm.ai"},"plugins":[{"name":"good-plugin","source":{"source":"git-subdir","url":"https://github.com/org/monorepo.git","path":"plugins/my-plugin"},"version":"1.0.0"}]}
HTTP 200

$ psql -d litellm -tAc "select name, coalesce(manifest_json,'<NULL>') from \"LiteLLM_ClaudeCodePluginTable\" order by name"
good-plugin|{"name": "good-plugin", "source": {"source": "git-subdir", ...}, "version": "1.0.0"}
null-manifest-plugin|<NULL>

The bad row is still there and is now skipped instead of crashing the response, which is what the file's two other read sites already did

Everything else in this PR is annotation-only, so the proof that matters is that behavior did not move. custom_openapi_spec.py is the file whose public signatures changed most, and it feeds /openapi.json, so I ran all four of its entry points at both commits over the live 1.25 MB spec this proxy serves and diffed the results:

$ curl -sS -o openapi.json -w "HTTP %{http_code}  bytes=%{size_download}\n" http://localhost:4000/openapi.json
HTTP 200  bytes=1254737

add_chat_completion_request_schema         byte-identical output: True
add_embedding_request_schema               byte-identical output: True
add_responses_api_request_schema           byte-identical output: True
add_llm_api_request_schema_body            byte-identical output: True

Type

🧹 Refactoring

🐛 Bug Fix

Changes

Eleven modules were picked for high Any density and low importer count, so the blast radius stays small. The types come from what the code already assumes: TypedDicts for JSON payloads read by literal key, Protocols for prisma rows, existing litellm/types/ models where the shape was already declared, and object where a value is only stored and forwarded. New annotations use read-only views rather than dict/list, so LIT001 falls alongside the Any counts instead of trading one budget for another. No suppressions, casts, or type guards were added anywhere

Tree-wide basedpyright goes 148012 to 147359, with reportAny down 362, reportExplicitAny down 122, and the four reportUnknown* rules down 145 between them. No rule increases anywhere in the tree, including in files this PR does not touch. Strict ruff drops 80, led by ANN401 down 59, and the LIT rules drop 85, led by LIT001 down 76. All three budget files are ratcheted by exactly those amounts

Some annotations from the first pass turned out to describe what the code wished were true rather than what flows through, and those are corrected here. _resolve_user_id claimed every request-body value was a Mapping while _resolve_trusted_user_id one method over types the same argument Mapping[str, object]. _CatoAnalyzeResponse.required_action was required and non-nullable though the API returns null, which seven fixtures in the guardrail's own suite already asserted. _PluginRecord.manifest_json was str against a nullable column, and making it honest is what surfaced the crash above. Two functions in ownership.py took an attribute Protocol while their own bodies branch on isinstance(response, dict), which no Protocol can satisfy

Where honesty cost precision, precision lost. _should_block went back to its original signature entirely: the narrowing needed to type it turned a fail-closed DLP control fail-open, because the TypeError it used to raise on a malformed Graph response reached except Exception in purview_dlp.py and became a 400. A slightly lower Any count is not worth letting traffic through a guardrail that used to reject it. _extract_field_schema went back to Any for the same reason, since the TypedDict proposed for it excluded $ref, items and enum keys that the function returns straight out of a Pydantic sub-schema

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Types the values that were flowing through as Any in the highest-density
modules, using shapes the code already assumes: TypedDicts for the JSON
payloads read by literal key, Protocols for the prisma rows, existing
litellm types where they were already modeled, and `object` where a value
is only stored and forwarded.

Annotation-level only, no runtime behavior change. New annotations use
read-only views (Mapping / Sequence / tuple) rather than dict / list, so
LIT001 drops alongside the Any counts instead of trading one budget for
another. No suppressions, casts, or type guards were added.

basedpyright across the touched files: 1547 -> 856 errors, with reportAny
down 399 and reportExplicitAny down 134, and no rule increasing.
…alues

An adversarial review of the previous commit found annotations that
described what the code wished were true rather than what flows through.
A false annotation is worse than the Any it replaced, since it launders a
wrong assumption past the type checker.

- purview: `_resolve_user_id` claimed every request-body value was a
  Mapping, contradicting `_resolve_trusted_user_id` one method over, which
  types the same argument `Mapping[str, object]`. `_should_block` claimed
  every Graph response value was a sequence of str->str mappings and was
  not assignable from its own producer's return type.
- cato: `_CatoAnalyzeResponse.required_action` was required and
  non-nullable while the API returns null, as seven fixtures in the
  guardrail's own suite assert. `analysis_result` had the same problem.
  The streaming hook narrowed an override parameter below what
  `ProxyLogging` actually passes it.
- marketplace: `_PluginRecord.manifest_json` was `str` against a nullable
  column. Making it honest surfaced a latent crash, covered below.
- ownership: two functions took an attribute Protocol while their own
  bodies branch on `isinstance(response, dict)`, which no Protocol can
  satisfy.
- openapi generator: `paths` claimed every path-item value was an
  operation, though path items also carry `parameters`, `summary` and
  `$ref`.
- custom openapi spec: a TypedDict asserted a shape that the function
  returns raw Pydantic sub-schemas out of. Reverted to Any, which is
  imprecise but not false.

`get_marketplace` did an unguarded `json.loads` on the nullable
`manifest_json` inside an `except json.JSONDecodeError`, which cannot
catch the TypeError a NULL raises, so one NULL row 500s the endpoint. It
now skips the plugin like the file's other two read sites already do, with
a regression test that fails without the guard.

Where honesty cost precision, precision lost. `_should_block` went back to
its original signature entirely: the narrowing needed to type it turned a
fail-closed DLP control fail-open, because the TypeError it used to raise
on a malformed response reached `except Exception` and became a 400.
Lowers the committed ceilings so the headroom shrinks by exactly what was
cleared instead of leaving stale slack for the next change to spend.

basedpyright -653 errors across 48 rules, with reportAny 29204 -> 28842
and reportExplicitAny 9227 -> 9105. Strict ruff -80 violations, led by
ANN401 -59. LIT rules -85, led by LIT001 -76.
@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR tightens annotations across caching, integrations, proxy utilities, guardrails, and policy modules while reducing static-analysis budgets. It also prevents nullable plugin manifests from breaking marketplace generation and adds regression coverage.

  • Replaces broad annotations with object, mappings, sequences, protocols, and typed payload structures.
  • Skips marketplace records whose nullable manifest lacks a usable source.
  • Ratchets basedpyright, Ruff, and type-discipline budgets to the reduced diagnostic counts.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py Refines OpenAPI payload annotations without changing the generator’s runtime control flow.
litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py Models nullable plugin manifests accurately and skips unusable marketplace rows instead of failing the endpoint.
tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py Adds focused regression coverage for marketplace rows with null manifests.
litellm/caching/redis_semantic_cache.py Narrows cache interfaces and internal values using callable, mapping, and object annotations.
litellm/integrations/galileo.py Adds typed logging payload fields and narrows callback, record, and serialization interfaces.
litellm/proxy/container_endpoints/ownership.py Introduces protocols for managed-object rows and tables while preserving ownership behavior.

Reviews (2): Last reviewed commit: "chore(lint): ratchet lint budgets down b..." | Re-trigger Greptile

Comment thread litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@codspeed-hq

codspeed-hq Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_reduce_any_types (ca7453b) with litellm_internal_staging (b66d4e6)

Open in CodSpeed

…itellm_reduce_any_types

# Conflicts:
#	basedpyright-code-budget.json
#	type-discipline-budget.json
The base branch ratcheted the same limits in 28a277e, so the conflicting
files were reset to base and the ratchet re-run against the new merge-base
rather than resolved by hand. Each limit is now the base value minus this
branch's own delta, so both ratchets survive: basedpyright -653 across 48
rules, strict ruff -80, LIT -85.

mateo-berri commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

proxy-infra is red on TestPriceDataReloadIntegration::test_distributed_reload_check_function, and it is not from this PR. It fails identically on the base branch

I ran the shard's exact file list with the same settings CI uses (-n 2 --reruns 2) on this branch and on base tip b66d4e69 in a separate worktree. Both produce the same 11 failures and the same totals, 11 failed, 6268 passed, 1 skipped, and the two FAILED lists diff clean:

FAILED tests/test_litellm/proxy/test_proxy_server.py::TestPriceDataReloadIntegration::test_complete_reload_flow
FAILED tests/test_litellm/proxy/test_proxy_server.py::TestPriceDataReloadIntegration::test_distributed_reload_check_function
FAILED tests/test_litellm/proxy/test_proxy_server.py::TestPriceDataReloadIntegration::test_every_pod_applies_a_manual_revision_exactly_once
FAILED tests/test_litellm/proxy/test_proxy_server.py::test_delete_config_general_settings_emits_deleted_audit_log
FAILED tests/test_litellm/proxy/test_proxy_server.py::test_update_config_general_settings_applies_ssrf_globals
FAILED tests/test_litellm/proxy/test_proxy_server.py::test_update_config_general_settings_emits_audit_log
FAILED tests/test_litellm/proxy/common_utils/test_config_sync_pubsub.py::test_evict_config_param_does_not_publish
FAILED tests/test_litellm/proxy/common_utils/test_key_rotation_e2e.py::TestDeprecatedKeyLookupDbE2E::test_deprecated_key_grace_period_cache_hit_path
FAILED tests/test_litellm/proxy/common_utils/test_periodic_reload_schedule.py::test_record_manual_reload_bumps_the_revision_atomically
FAILED tests/test_litellm/proxy/common_utils/test_periodic_reload_schedule.py::test_record_reload_run_updates_last_run_without_creating_or_bumping
FAILED tests/test_litellm/proxy/common_utils/test_periodic_reload_schedule.py::test_write_reload_interval_touches_only_param_value

tests/test_litellm/proxy/test_proxy_server.py passes whole-file on its own here, 303 passed, and the flagged test passes in isolation. These only fail under xdist, and they all mutate process-global state (litellm.model_cost, config params, the reload schedule), so which worker touches it first decides the outcome. The failing assertion is litellm.model_cost["gpt-3.5-turbo"] == {"input_cost_per_token": 0.001} receiving the full unreplaced entry, which is another worker having repopulated the map

Nothing in this PR touches proxy_server.py, get_model_cost_map.py, or the cost map. Happy to re-run once base is green, and if it would help I can open a separate issue for the shared-global-state pollution in that shard

@mateo-berri
mateo-berri merged commit 9e7b057 into litellm_internal_staging Aug 6, 2026
83 of 85 checks passed
@mateo-berri
mateo-berri deleted the litellm_reduce_any_types branch August 6, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants