Skip to content

refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13, 2026-09-16, 2026-09-17, 2026-09-18, 2026-09-19, 2026-09-20, 2026-09-22) - #40030

Closed
devin-ai-integration[bot] wants to merge 26 commits into
mainfrom
litellm_techdebt_20260906

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Fresh suppressions and slop landed on main that fix trivially
  • 2026-09-06: a banned cast import, two cast-ok casts, two mutable-ok bare dict params
  • 2026-09-09: a getattr on an already narrowed type, two mutable-ok, one section-header comment
  • 2026-09-10: two dict[str, Any] returns, a getattr on a declared Delta field, a copy of the Anthropic content traversal that already lives in common_utils
  • 2026-09-13: four getattr reads on declared fields
  • 2026-09-16: 14 suppression comments that suppress nothing, two over-scoped pyright ignores, one misleading rebind-ok reason
  • 2026-09-17: a dead rebind-ok, a noqa on the wrong line, a getattr on a declared field, a bare list return
  • 2026-09-18: a getattr on a declared field, four unannotated module TypeVars, two implicit type aliases, four Any annotations in the member budget helper
  • 2026-09-19: three Any annotations, four bare dict / list signatures, one dead public function
  • 2026-09-20: six suppression trailers on lines the gate never reads, two noqa: BLE001 that suppress nothing, a bare dict param, a getattr on a declared SDK field
  • 2026-09-22: three getattr reads on declared fields behind a one-use Protocol, one headers: Any, one bare dict return

How it solves it:

  • Narrow the ContentBlockDelta | MessageBlockDelta union on its type literal and rebuild the refusal chunk by spreading it
  • Drop the redundant Mapping cast that followed an isinstance(container, Mapping) check
  • Type the two read-only Lyria helpers as Mapping[str, object]
  • Read callback.callback_name directly after the isinstance(callback, OpenTelemetryV2) check
  • Build the MCP identity TypedDict under a Final[MCPProxyToolIdentity] annotation and branch on tool.meta is None
  • Delete the # TwelveLabs Marengo Embed types comment
  • Type the two Anthropic Responses payload builders as dict[str, object]
  • Read cached_choice.delta.tool_calls directly in the cached-completion replay
  • Export anthropic_content_lists from common_utils and make the router affinity check iterate through it
  • Read litellm_params, auth_type, litellm_request_debug and provider_specific_fields directly off their typed objects
  • Delete mutable-ok and pyright: ignore trailers that the gates verifiably no longer need, and narrow the two ignores that listed a rule that never fires
  • Drop the dead rebind-ok, move the noqa: PLC0415 onto the from line ruff reports, read presidio_filter_scope off LitellmParams, and type the fallback chain as list[str] | None
  • Read litellm_request_debug off Logging in the new Rust bridge callback gate, mark the bridge TypeVars Final, declare NativeHook and JevProbability as TypeAlias, and type the member budget patch as Mapping[str, object]
  • Type the A2A registry headers, the two pricing-field helpers and the guardrail stream events as object-valued, type the Anthropic-only tool key stripper, the aggregate authorization server metadata and the web search tool_args as dict[str, object], and delete the uncalled pending_days
  • Move the six mutable-ok / rebind-ok trailers onto the statement line the type-discipline gate reports, delete the two inert noqa: BLE001, type the hoisted-system probe input as Mapping[str, object], and read result.next_cursor off ListToolsResult
  • Read proxy_logging_obj.db_spend_update_writer.pod_lock_manager directly and drop the _PodLockManager Protocol, read audio_tokens off the typed token-details wrappers, type the Vercel embedding error headers as dict[str, str] | httpx.Headers, and type the budget reservation lookup as dict[str, object] | None

User Flow

Refactor only, no user-visible behavior changes. The refusal message_delta chunk keeps the same keys and delta contents, OTel v2 logger reuse matches the same callback names, MCP virtual tool ids resolve the same way, the Bedrock type aliases are unchanged, and encrypted-content affinity routing sees the same content blocks, OCR cost lookups, MCP pass-through admission, the Rust bridge callback gate and hosted web search replay all take the same inputs and return the same results. The 2026-09-16 run only removes or rewords comments, apart from the refusal chunk rebuild described under that date. The 2026-09-17 run touches comments and annotations only, plus one attribute read that resolves to the same value. The 2026-09-18 run is annotations only, plus one attribute read that resolves to the same value. The 2026-09-19 run is annotations only, plus the deletion of a function nothing called. The 2026-09-20 run moves or deletes comments, changes one annotation, and replaces one attribute read that resolves to the same value. The 2026-09-22 run is annotations only, plus three attribute reads that resolve to the same values

Findings by run date

2026-09-22 (window 2026-09-21 07:43 UTC to 2026-09-22 07:43 UTC, 339 commits, 1,186 files)

  1. 8795be0 refactor(types): replace Any with proven types in 32 files: initialize_mavvrik_focus_export_job in litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py gained a one-use _PodLockManager Protocol wrapping getattr(proxy_logging_obj, "db_spend_update_writer", None) and getattr(writer, "pod_lock_manager", None). proxy_logging_obj is declared ProxyLogging, whose __init__ always assigns db_spend_update_writer: DBSpendUpdateWriter, which always assigns pod_lock_manager: PodLockManager, so the lock manager is now read directly under a Final and the Protocol is deleted
  2. 537e8ac feat(cost): warn and count $0 cost on billable requests: _audio_tokens(details: object) in litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py read getattr(details, "audio_tokens", None). Its only two callers pass Usage.prompt_tokens_details and Usage.completion_tokens_details, both declared wrappers with an audio_tokens: int | None field, so the parameter is PromptTokensDetailsWrapper | CompletionTokensDetailsWrapper | None and the read is a plain attribute read behind a None check
  3. 8795be0 same commit: get_error_class in litellm/llms/vercel_ai_gateway/embedding/transformation.py was rewritten with headers: Any. The base BaseConfig.get_error_class declares headers: dict | httpx.Headers; the override takes dict[str, str] | httpx.Headers, which is what httpx.Response.headers carries and what VercelAIGatewayException accepts. httpx was already imported
  4. e7cd97c fix(proxy): release unclaimed budget reservations at request end: budget_reservation_from_metadata in litellm/litellm_core_utils/core_helpers.py returned a bare dict | None. The reservation is a string-keyed payload, so it is dict[str, object] | None; its callers in proxy_track_cost_callback.py accept dict | None unchanged

Follow-up on the 2026-09-20 next_cursor item: CI's test_mcp_client_unit.py::test_list_tools feeds a MagicMock result, whose truthy mock cursor sent the walk into a second page and an empty list. 018a6a9 keeps the typed result.next_cursor read and restores the isinstance(next_cursor, str) guard so a non-string cursor still ends the walk, matching the pre-cleanup behavior

Deliberately left alone: the **kwargs: Any and the seven noqa: PLC0415 in litellm/proxy/_experimental/mcp_server/operations.py (1098604) are lines moved out of server.py, not new code. _get_config_value(...) -> Any | None in generic_guardrail_api/__init__.py (8795be0) reads an arbitrary attribute by name, so a narrower type needs a typed config model, not a one-line change. should_report_bug in bug_report.py reads status_code off a BaseException that does not declare it. The section-header comments in litellm/litellm_core_utils/tokenizer.py (0abd926) follow a pattern used in several hundred places in the tree, so removing five is taste. The #### banner in user_api_key_auth_mcp.py (82eef2f) matches the sibling banners in the same function. The multi-line comments added in the TinyFish, MCP OAuth, parallel-request-limiter and pass-through timeout code document billing, authorization and timeout contracts rather than restating code. No budget ceiling was raised in the window, no type: ignore was added, and no bare TODO or FIXME landed

2026-09-20 (window 2026-09-19 07:43 UTC to 2026-09-20 07:43 UTC, 205 commits, 1,118 files)

Suppression trailers that suppress nothing. scripts/check_type_discipline.py honours a *-ok trailer only on the exact line it reports, which for a multi-line statement is the first line. Each trailer below sat on the closing line, so the violation it meant to justify still counted against type-discipline-budget.json while the comment did nothing

  1. ed40241 fix(proxy): estimate auto-router baseline costs from durable cache history: # mutable-ok: spend-log JSON serialization requires plain mappings on the closing } of the autorouter_savings_estimate literal in litellm/litellm_core_utils/litellm_logging.py. Moved onto the opening { the gate reports
  2. ed40241: # rebind-ok: keyset pagination advances after each complete timestamp group on the cursor = page[-1].started_at rebinding in litellm/proxy/db/baseline_accounting.py, while LIT010 fires on the cursor: float | None = None declaration. Moved onto the declaration
  3. ed40241: # rebind-ok: drain under lock on the closing ] of the client.baseline_accounting_transactions = ...[32:] slice in baseline_accounting.py. Moved onto the assignment line
  4. ed40241: # noqa: BLE001 # unknown acknowledgements can be replayed safely on except (Exception, asyncio.CancelledError) as error: in baseline_accounting.py. Ruff does not raise BLE001 there because the handler re-raises, so the trailer is inert. Deleted
  5. ed40241: # rebind-ok: request-owned retry marker and # rebind-ok: capture uncertainty for failure logging on the closing lines of the two logging_obj.* assignments in litellm/proxy/hooks/autorouter_baseline_cache.py. Moved onto the assignment lines; the second reason is shortened to uncertain capture for failure logs so the line fits the 120-column formatter
  6. 82e3f39 refactor(auth): resolve org identity through an auth_checks helper: # noqa: BLE001 # only a DB outage may fail auth here, ... on except Exception as e: in litellm/proxy/auth/auth_checks.py. Ruff exempts the handler because it logs with exc_info=True, so the trailer is inert. Deleted
  7. 99b1a32 feat(guardrails): add headroom guardrail for message compression: _hoisted_top_level_system_message(self, data: dict) in litellm/llms/anthropic/chat/guardrail_translation/handler.py only calls data.get, and its # mutable-ok: API message payload sat on the return-type line where LIT001 never looks. Typed as Mapping[str, object] and the trailer deleted
  8. 5dc0131 refactor(mcp): port MCP client and server helpers to MCP SDK 2: next_cursor = getattr(result, "next_cursor", None) plus an isinstance(next_cursor, str) guard in litellm/experimental_mcp_client/tools.py. mcp.types.ListToolsResult.next_cursor is declared str | None on the pinned mcp>=2.2.0, so the read is result.next_cursor and the guard is if not next_cursor

Net effect on the gates at the tip: type-discipline violations in the six touched files drop from 1057 to 1051, basedpyright errors in them from 2070 to 2065, ruff-strict unchanged at 309. No budget file was edited (the working-tree counts sit under every ceiling and AGENTS.md asks not to touch budgets on a PR branch)

Deliberately left alone: video_tokens=getattr(response_api_usage.input_tokens_details, "video_tokens", None) in litellm/responses/utils.py matches nine sibling getattr reads that predate the window, so swapping one line would be a drive-by. tool_call_map: Final[dict[_ToolCallKey, dict[str, Any]]] in streaming_chunk_builder_utils.py and extract_file_data(cast(Any, ...)) in files_endpoints.py need a typed tool-call payload model, not a one-line change. The MCP getattr/setattr reads in mcp_server/utils.py, sampling_handler.py and elicitation_handler.py deliberately accept both mapping and SDK-object shapes, so direct access would change what a mapping result does. prisma_client: Final[PrismaClient] = getattr(managed_files_obj, "prisma_client") reads a field BaseFileEndpoints does not declare. The getattr(_native, ...) lookups in rust_bridge/fork_guard.py probe an optional native extension. The new multi-line comments in encrypt_decrypt_utils.py, experimental_mcp_client/client.py, proxy_cli.py and the OpenAI Responses guardrail handler explain compatibility or forking contracts. No budget ceiling was raised in the window, no type: ignore was added, and no bare TODO or FIXME landed

2026-09-19 (window 2026-09-18 07:43 UTC to 2026-09-19 07:43 UTC, 245 commits, 881 files)

  1. 1c40e60 fix(a2a): Entra credentials own the chat route bearer over a stored api_key or authorization header: _registry_headers in litellm/llms/a2a/chat/transformation.py returned dict[str, Any] | None for a filtered copy of a Mapping[str, object]. Typed as dict[str, object] | None
  2. b03957b fix(proxy): unpin cost-map pricing copied into model_info and report pricing overrides: echoed_cost_map_pricing_fields and pricing_override_fields in litellm/types/utils.py took Mapping[str, Any] but only read keys and compare values to None. Typed as Mapping[str, object]
  3. b3d9ba9 fix(policy_engine): deliver guardrail text rewrites on multi-choice, unfinished, and envelope-less streams: _spread_text_rewrite_over_stream_events in litellm/llms/openai/responses/guardrail_translation/handler.py took stream_events: Sequence[Any] and only passes each event to stream_item_field(item: object, ...). Typed as Sequence[object]
  4. 1adbfbf fix: strip eager_input_streaming for non-Claude providers next to input_examples: _without_anthropic_only_tool_keys(tool: dict) -> dict and _drop_anthropic_only_tool_keys(tools: list[dict] | None) in litellm/main.py were bare. Typed as dict[str, object] and list[dict[str, object]]
  5. 7cb01cf fix(proxy): exchange role default, logged rejections, gated grant listing: the rewritten _build_aggregate_authorization_server_response signature in litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py kept a bare -> dict. Typed as dict[str, object]; the sibling _build_aggregate_protected_resource_response -> dict predates the window and is left alone
  6. 5ef05b9 feat(websearch): let the model emit objective + multi-query search shape for providers that support it: tool_args: dict | None in litellm/integrations/websearch_interception/handler.py. Typed as dict[str, object] | None, keeping its mutable-ok
  7. c8a2d8c feat(proxy): add LiteLLM_DailyGlobalSpend key-free rollup for the usage dashboard: pending_days in litellm/proxy/spend_tracking/daily_global_spend_rollup.py had no caller in litellm/, enterprise/, tests/ or docs/. Deleted

Deliberately left alone: generation_config: dict[str, Any] | None in litellm/llms/gemini/interactions/transformation.py, because the popped image_config is spread into a dict and typing it as object needs an isinstance guard that would change what a non-dict value does. The getattr reads on incomplete_details, fallback_headers_adopted, param_value and guardrail_name read fields their declared types (ModelResponse, a response union, an Any config row, object) do not carry. The LoggingSurface getattr/setattr on private _defer_async_logging and _native_pending_logging in legacy_callbacks.py is the Rust bridge's adapter contract, not a same-day cleanup. Every new noqa, pyright: ignore, mutable-ok and rebind-ok in the window names one rule and carries a reason. No budget ceiling was raised, no type: ignore, and no bare TODO or FIXME was added. Merging main also dropped the 2026-09-18 finding 1: main rewrote legacy_callbacks.py and callbacks_needed with its litellm_request_debug read no longer exists, so the branch now carries main's version of that file unchanged

2026-09-18 (window 2026-09-17 07:43 UTC to 2026-09-18 07:43 UTC, 149 commits, 680 files)

  1. 63d994a refactor(rust): run OCR through a route-neutral callback contract and a legacy Logging adapter: callbacks_needed in litellm/rust_bridge/legacy_callbacks.py read getattr(logger, "litellm_request_debug", False). Logging.litellm_request_debug is declared with a False default (the 2026-09-13 run fixed the same read in lifecycle.py before it moved here), so it is a plain attribute read
  2. 9617312 add PublicDispatch and c01db25 bring x-litellm-rust: RequestT, NativeT, ResultT in litellm/rust_bridge/dispatch.py and ResultT in litellm/rust_bridge/response_metadata.py were module-level TypeVars without : Final (LIT010). Annotated
  3. 9617312 and 0a66328 fix(router): validate Jev classifier probabilities: NativeHook in dispatch.py and JevProbability in litellm/router_strategy/complexity_router/jev_classifier.py were implicit aliases assigned at module level. Declared with : TypeAlias
  4. 648373a feat(management_v1): bulk update team member budgets: member_budget_patch in litellm/proxy/management_endpoints/common_utils.py returned dict[str, Any], _upsert_budget_and_membership took budget_patch: dict[str, Any], and its source and create_data locals were Any-valued. The patch is only read, so it is Mapping[str, object] on both ends and the locals are object-valued; the mutable-ok on create_data stays because the Prisma payload is still built as a dict

Deliberately left alone: the dict[str, object] parameters on MetadataUpdater and finalize in legacy_callbacks.py, because update_response_metadata writes into that kwargs dict, so Mapping would misdescribe the contract. The getattr reads in litellm_logging.py (_hidden_params), ocr/main.py (file_input.name), rust_bridge/ocr/route_host.py (error.headers, ocr_request_format_error) and model_management_endpoints.py (iterated field names) read fields their declared types do not carry. The requested_query_params: dict | None rebinding in pass_through_endpoints.py is pre-existing legacy flow that the window only extended. The multi-line crash-recovery comment in autoroute/commands.py and the inherited-window comment in common_utils.py explain non-obvious behavior rather than restating code. Every new pyright: ignore in the window names one rule and carries a reason. No budget ceiling was raised in the window, no type: ignore, and no bare TODO or FIXME was added

2026-09-17 (window 2026-09-16 04:19 UTC to 2026-09-17 07:43 UTC, 372 commits since the first-parent base a8979fe)

Every suppression added in the window (98 in non-test files) was stripped and the matching gate re-run on its file, as the 2026-09-16 run did. Two produced no violation without their comment; the other 96 reproduce their violation and stay. All seven new pyright: ignore trailers list exactly the one rule that fires, so nothing to narrow

  1. 4cb4493 fix(proxy): let the OTel trace id fallback fill a null litellm_trace_id: # rebind-ok: metadata is the request's own out-param dict on metadata["trace_id"] = trace_id in litellm/proxy/litellm_pre_call_utils.py suppressed nothing (LIT011 only flags the data[...] store two lines up, whose marker stays). Removed
  2. ee676d5 fix(mcp): preserve browser OAuth for unrelated bearer tokens: in litellm/proxy/_experimental/mcp_server/bridge_token_flow.py the # noqa: PLC0415 # envelope imports bridge types sat on the is_envelope, name inside a parenthesized import, but ruff reports PLC0415 on the from line, so the import was unsuppressed. Moved the trailer onto the from line
  3. 47117d8 fix(guardrails): don't add post_call output scan for MCP-only Presidio modes: initialize_presidio in litellm/proxy/guardrails/guardrail_initializers.py read getattr(litellm_params, "presidio_filter_scope", None). LitellmParams inherits PresidioConfigModel, which declares the field with a None default, so it is a plain attribute read
  4. 2b6184d fix(proxy): resolve rate-limit fallbacks after model normalization and retry from a client-request snapshot: _resolve_fallback_models in litellm/proxy/common_request_processing.py returned bare list | None; get_fallback_model_group already types the chain as list[str] | None, so the return is now list[str] | None. The fallbacks: list parameter stays because get_fallback_model_group itself takes list[Any]

Deliberately left alone: the getattr(chunk, "choices", ()) added by 884e969 in streaming_chunk_builder_utils.py looks redundant against its declared ModelResponse | ModelResponseStream union, but test_stream_chunk_builder_tolerates_trailing_chunk_without_choices feeds a BaseLiteLLMOpenAIResponseObject with no choices, so the annotation is narrower than the runtime input and the getattr is load-bearing; widening the annotation is a follow-up for that module. Also left: the # Check RBAC and router cooldown comments and the data: Any in safe_json_dumps.py, which the diff shows as added but are moved lines that already existed at the base; the bare Sequence in prometheus._set_customer_budget_metrics and the dict params in gen_ai_semconv and converse_transformation, which mirror the untyped shapes they receive; the three data: dict seeds in proxy_server.py, which are rebound in legacy handlers. No budget ceiling moved, no type: ignore, and the only new TODO links LIT-5722

2026-09-16 (window 2026-09-15 07:43 UTC to 2026-09-16 07:43 UTC, 173 commits)

Every suppression added in the window was stripped one at a time and the matching gate re-run on the file (scripts/check_type_discipline.py, ruff check --config ruff-strict.toml, basedpyright with the type_check_gate.py invocation). The ones below produced no violation without their comment, so the comment was a dead marker and is removed. Every other suppression in the window (164, including every noqa) reproduces its violation when stripped and stays

  1. cccff65 fix(guardrails): scan the Anthropic top-level system prompt and tool_use arguments: eight # mutable-ok: guardrails rewrite the caller's request payload in place trailers in litellm/llms/anthropic/chat/guardrail_translation/handler.py sat on item stores (block["input"] = ..., content[i]["text"] = ...) that LIT001/LIT002 never flag, and one on a list comprehension bound to Final. Removed; ruff format collapsed the parenthesized continuations back to single lines
  2. 69be041 feat(proxy): add /nvidia_nim passthrough route for NIM object detection and OCR /v1/infer: the # mutable-ok on the returned dict literal in litellm/llms/nvidia_nim/passthrough/transformation.py (line 113) suppressed nothing. Removed; the two on the signature and the dict(headers) call are needed and stay
  3. ff5b59b feat(proxy): add POST /user/bulk_new for batched user and team membership creation: in litellm/proxy/management_helpers/bulk_user_creation.py a # pyright: ignore[reportUnknownArgumentType] on the _set_object_permission call and a # mutable-ok on a list literal already wrapped in tuple(...) were dead. Removed. The ignore on team.metadata listed reportUnknownArgumentType alongside reportUnknownMemberType; only the latter fires, so the code list is narrowed
  4. cd9c399 feat(proxy): add POST /user/bulk_delete and POST /team/bulk_member_delete: # pyright: ignore[reportUnknownArgumentType] # HTTPException.detail is untyped in litellm/proxy/management_helpers/bulk_user_deletion.py (line 190) sat on str(exc.detail), which type-checks. Removed; the neighbouring ignore two lines up still fires and stays
  5. a6a58b3 feat(router): add Switchyard capability classifier: in litellm/router_strategy/complexity_router/complexity_router.py a # mutable-ok on a dict literal already annotated Final[StandardLoggingRoutingDecision] (which LIT002 exempts) and one on a .append(...) call (method calls are out of the checker's reach) were dead. Removed
  6. 7cc07d4 fix(responses): build the billed terminal response immutably and guard the cache dump: the ignore in litellm/responses/streaming_iterator.py listed reportUnknownArgumentType, reportArgumentType; only reportArgumentType fires, so the list is narrowed
  7. Reason wording: # rebind-ok: the counter update reads the stamp off the shared dict on budget_reservation["finalized"] = True in litellm/proxy/hooks/proxy_track_cost_callback.py described a read, while the marker suppresses an in-place parameter store. Reworded to stamps the caller's shared dict for the counter update
  8. Rolling-branch fix, not a window finding: the 2026-09-06 rewrite of _with_refusal_stop_details (cdea9c9) read processed_chunk["usage"] on a NotRequired key, which raised KeyError for a message_delta without usage and pushed reportTypedDictNotRequiredAccess two over its ceiling on this branch (main sits exactly at the limit). The chunk is now rebuilt as refusal_chunk: Final[MessageBlockDelta] = {**processed_chunk, "delta": refusal_delta}, which is what the pre-rewrite code did with a cast, so usage and context_management pass through whether or not they are present

Deliberately left alone: the dict[str, Any] / Mapping[str, Any] parameters across litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py and litellm/llms/openai/chat/guardrail_translation/handler.py (raw request payloads with no typed shape to read through), and the getattr reads in exception_mapping_utils.py, reset_budget_job.py and the pass-through handlers, which read fields the declared types do not carry. No budget ceiling was raised in the window, no type: ignore, and no bare TODO or FIXME was added

2026-09-13 (window 2026-09-12 07:43 UTC to 2026-09-13 07:43 UTC, 65 commits)

  1. 45fad44 fix(cost): bill request-level OCR pricing on direct SDK calls: _deployment_model_info and _ocr_model_info in litellm/cost_calculator.py read getattr(litellm_logging_obj, "litellm_params", None). Logging.__init__ always assigns litellm_params, so both are plain attribute reads now; the None guard on the logging object stays. Because the field is a non-optional dict, the follow-up check is if not litellm_params, which returns None for the same inputs (an empty dict never yielded a model_info anyway)
  2. 94e4dd7 fix(mcp): require admission for delegated OAuth: litellm/proxy/_experimental/mcp_server/mcp_server_manager.py filtered the registry with getattr(server, "auth_type", None). MCPServer.auth_type is a declared field, so it is server.auth_type now
  3. 347b642 refactor(ocr): complete native lifecycle and preserve Azure auth: callbacks_needed in litellm/rust_bridge/lifecycle.py read getattr(logger, "litellm_request_debug", False). Logging.litellm_request_debug is declared with a False default, so it is a plain read
  4. eddfb5f fix(responses): preserve hosted web search calls: _web_search_calls_by_call_id in litellm/responses/litellm_completion_transformation/transformation.py read getattr(choice.message, "provider_specific_fields", None). Message.provider_specific_fields is declared, so it is a plain read; the isinstance(..., Mapping) guard still handles None
  5. 76cb0fe fix(model_management): stop persisting cost map pricing as a deployment override: without_server_derived_pricing in litellm/types/utils.py takes Mapping[str, Any]. Started as finding 5 and reverted (1bc2438): the object signature forces the /model/new caller to build ModelInfo through model_validate instead of ** unpacking, which reviewers cannot verify at a glance, so it stays as reported debt

Also committed: ui/litellm-dashboard/src/lib/http/schema.d.ts loses two stale soft_budget doc lines. That is the generator output for the merged tip (the docstring was removed on the base branch without regenerating), and make check fails until it is in the tree

Deliberately left alone: getattr(logger, "logger_fn", None) in the same callbacks_needed function and getattr(logging_obj, "call_type", None) plus getattr(logging_obj, "_native_pending_logging", None) in common_request_processing.py (347b642): logger_fn is not declared on Logging, logging_obj is typed Any on a pre-existing signature, and _native_pending_logging is set from Rust (litellm-rust/crates/python-bridge/src/lifecycle/bindings.rs), so there is no static type to read through. getattr(updated_patch.litellm_params, field) in model_management_endpoints.py (76cb0fe) iterates over field names, so the string access is inherent. getattr(event, "output_index", None) and getattr(tool_call, "id", None) in the Responses bridge (eddfb5f) read fields the base object/BaseLiteLLMOpenAIResponseObject types do not declare. The mutable-ok on the web search call index, the rebind-ok on the OCR release signal and the two reportPrivateUsage ignores in lifecycle.py all carry reasons and have no static replacement. The multi-line OAuth and admission comments added in mcp_server/server.py, budget_reservation.py, key_management_endpoints.py, user_api_key_cache.py and proxy_cli.py document security and rollout contracts rather than restating code. The # Run the coroutine in its original context comment in logging_worker.py predates the window (only re-indented). .github/scripts/*.py gained Any but sits outside the typed litellm/ tree. No budget ceiling was raised in the window, no type: ignore, and no bare TODO/FIXME was added

2026-09-10 (window 2026-09-09 07:17 UTC to 2026-09-10 07:43 UTC, 225 commits)

  1. 2bca7ff fix(anthropic): replay OpenAI encrypted reasoning byte for byte behind /v1/messages: _thinking_block_from_reasoning_item and _assistant_group_to_input_items in litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py returned dict[str, Any]. The values are only ever serialized, so both are now dict[str, object]; the mutable-ok trailers stay because the payloads are still built as dicts
  2. 29af8b7 fix(streaming_handler): replay a cached completion with no choices as an empty stream: litellm/litellm_core_utils/streaming_handler.py read getattr(cached_choice.delta, "tool_calls", None). Delta.tool_calls is a declared field, so it is a plain attribute read now
  3. e27a018 fix(router): pin bridge-replayed encrypted reasoning to the deployment that minted it: _anthropic_content_blocks in litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py re-implemented the message walk that _anthropic_content_lists in litellm/litellm_core_utils/prompt_templates/common_utils.py already does (2bca7ff). The helper is now public as anthropic_content_lists and the router check iterates through it; the module already imported from common_utils, so no new import edge

Deliberately left alone: three candidates were started and then reverted (2d0ee09) once git log -S showed they were pre-existing lines only re-indented or moved in the window, so out of scope: getattr(response_obj, "usage", None) (249a999) and getattr(usage_obj, "cost", None) (9e25dd7) in litellm/responses/streaming_iterator.py, and the # Use completion_cost comment in litellm/proxy/management_endpoints/cost_tracking_settings.py. The usage read also cannot become a plain attribute without breaking test_responses_streaming_does_not_reset_prior_completion_start_time, which drives the iterator with Mock(spec=ResponsesAPIResponse). The remaining mutable-ok and cast-ok trailers added in the window sit on untyped client JSON or on API message payloads that are built as dicts and have no static replacement. No budget ceiling was raised in the window and no bare TODO/FIXME or type: ignore was added

2026-09-09 (window 2026-09-08 07:43 UTC to 2026-09-09 07:43 UTC, 87 commits)

  1. d82eb33 feat(otel): typed semconv-aligned OpenTelemetry instrumentation: _maybe_construct_otel_v2 in litellm/litellm_core_utils/litellm_logging.py used getattr(callback, "callback_name", None) right after isinstance(callback, OpenTelemetryV2). OpenTelemetryV2.__init__ always sets callback_name, so it is now a plain attribute read
  2. 754a2af feat(mcp): add schema discovery proxy mode: _mcp_proxy_identity in litellm/proxy/_experimental/mcp_server/tool_search.py carried # mutable-ok: absent metadata default on (tool.meta or {}).get(...). Now None if tool.meta is None else tool.meta.get(...), same result for None and for an empty mapping
  3. 754a2af same commit: the same function returned a dict literal under # mutable-ok: TypedDict identity payload. It is now assigned to resolved: Final[MCPProxyToolIdentity] and returned, which the LIT001 gate recognizes as a TypedDict build without a suppression
  4. dc09d9e feat(bedrock): add TwelveLabs Marengo Embed 3.0 embeddings: litellm/types/llms/bedrock.py gained a # TwelveLabs Marengo Embed types section header above two aliases whose names already say that. Deleted

Deliberately left alone: _group_deployments in litellm/router_strategy/complexity_router/complexity_router.py (0175c7d) reads get_model_list through getattr because the existing tests inject fake routers without that method, so a direct call fails them. attribute_of in litellm/proxy/common_utils/openai_error_payload.py and the getattr(guardrail_to_apply, name, default) in unified_guardrail.py take dynamic attribute names, so there is no static replacement. The rebind-ok loop accumulators in litellm/batches/batch_utils.py need a functional rewrite of the whole loop to remove, which is not a same-day cleanup. The cast-ok casts in litellm/proxy/utils.py preserve the legacy callback tuple contract. No budget ceilings were raised in the window (every limit moved down) and no bare TODO/FIXME or type: ignore was added

2026-09-06 (window 2026-09-05 07:43 UTC to 2026-09-06 07:43 UTC, 108 commits)

  1. 7399b38 fix(anthropic): satisfy lint budget gates for refusal translation: litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py imported cast under # noqa: TID251 and used two cast-ok casts in _with_refusal_stop_details to rebuild the refusal message_delta. Replaced with processed_chunk["type"] != "message_delta" narrowing, a MessageDelta typed delta, and a Final[MessageBlockDelta] spread of the chunk with the new delta (the explicit MessageBlockDelta(...) constructor from the first pass read usage unconditionally; see 2026-09-16 finding 8). The cast import is gone from the file
  2. 79d4778 fix(anthropic): stream the refusal text on bridged /v1/messages calls: _mapping_field in litellm/llms/anthropic/experimental_pass_through/messages/utils.py cast container to Mapping[str, object] right after isinstance(container, Mapping). Dropped the cast; container.get(key) type-checks as is
  3. 6ec53f2 style(vertex-ai): satisfy Lyria quality gates: _is_audio_predict_response and _get_audio_prediction_count in litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py took json_response: dict # mutable-ok while only reading it. Now Mapping[str, object], no suppression. _handle_audio_predict_response keeps dict because it stores the response into StandardPassThroughResponseObject

Deliberately left alone: the repeated is_non_conversational_call_type(...) and not self.inspect_embeddings skip in the AIM and Cato hooks (6e05ac5): the predicate is already shared, a wrapper would save one line per hook. truncate_base64_in_messages_async's str | list | dict | None mirrors the sync helper's contract. getattr(mcp_server, "per_server_oauth_discovery", False) follows the surrounding pattern for the loosely typed server object. No budget ceilings were raised in the window (every limit moved down)

Relevant issues

None

Affected release

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests (refactor only, existing tests pin the behavior)
  • The handful of test files covering my change pass locally (tests/test_litellm/rust_bridge/test_dispatch.py, tests/test_litellm/rust_bridge/test_legacy_callbacks.py, tests/test_litellm/rust_bridge/test_lifecycle.py, tests/test_litellm/router_strategy/complexity_router/test_jev_classifier.py, tests/test_litellm/proxy/management_endpoints/test_common_utils.py, tests/test_litellm/proxy/test_litellm_pre_call_utils.py, tests/test_litellm/proxy/test_common_request_processing.py, tests/test_litellm/proxy/guardrails/test_init_guardrails.py, tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py, tests/test_litellm/proxy/_experimental/mcp_server/test_oauth_identity_binding.py, tests/test_litellm/llms/anthropic/chat/guardrail_translation/, tests/test_litellm/llms/nvidia_nim/passthrough/, tests/test_litellm/proxy/management_helpers/test_bulk_user_creation.py, tests/test_litellm/proxy/management_helpers/test_bulk_user_deletion.py, tests/test_litellm/router_strategy/test_complexity_router.py except the semantic-tier cases that need the optional semantic_router package, tests/test_litellm/responses/test_streaming_iterator.py, tests/test_litellm/proxy/hooks/test_proxy_track_cost_callback.py, tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/, tests/unit/integrations/mavvrik_focus/test_mavvrik_focus_logger.py, tests/test_litellm/litellm_core_utils/llm_cost_calc/test_zero_cost_diagnostic.py, tests/unit/llms/vercel_ai_gateway/embedding/test_vercel_ai_gateway_embedding.py, tests/test_litellm/litellm_core_utils/test_core_helpers.py, plus the files listed by earlier runs)
  • My PR passes all required CI/CD checks (make lint, make check green locally at 018a6a9; budgets unchanged)
  • 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

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

Refactor only, so the proof is the debt count each fix removes, measured at the merge base and at the tip on the four files this run touched. Command at each sha: for f in litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py litellm/llms/vercel_ai_gateway/embedding/transformation.py litellm/litellm_core_utils/core_helpers.py; do git show <sha>:$f | grep -c 'getattr(\|Protocol)\|headers: Any\|) -> dict | None'; done. Earlier runs' files are unchanged since their own before/after in the PR history

Before (25af172, merge base)

  1. mavvrik_focus_logger.py: 3 (two getattr reads and the _PodLockManager Protocol)
  2. zero_cost_diagnostic.py: 1 (getattr(details, "audio_tokens", None))
  3. vercel_ai_gateway/embedding/transformation.py: 1 (headers: Any)
  4. core_helpers.py: 1 (-> dict | None)

After (018a6a9, PR tip)

  1. mavvrik_focus_logger.py: 0
  2. zero_cost_diagnostic.py: 0
  3. vercel_ai_gateway/embedding/transformation.py: 0
  4. core_helpers.py: 0
  5. make lint and make check pass at the tip with no budget file changes

Type

🧹 Refactoring

Caveats (if any)

Low

  • 2026-09-16 finding 8 is a behavior fix inside this branch's own earlier commit, not a cleanup of main; without it a refusal message_delta missing usage raised KeyError

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

Note

Low Risk
Refactor-only annotation and comment cleanup with attribute reads equivalent to prior getattr paths; risk is limited to edge cases like mock-based tests (e.g. MCP list_tools cursors), which the branch already guards.

Overview
This PR is a type-discipline and lint-debt cleanup across the litellm/ tree: no intended product behavior changes beyond one fix to refusal message_delta rebuilding so optional usage is not accessed unsafely.

Typing and direct access: Bare dict/Any/getattr on already-narrowed types are replaced with concrete annotations (dict[str, object], Mapping[str, object], typed headers) and plain attribute reads—for example litellm_params, MCP auth_type/next_cursor, OTel callback_name, provider_specific_fields, and Mavvrik pod-lock wiring without a one-off Protocol.

Structure and dead code: _anthropic_content_lists is exported as anthropic_content_lists and reused from encrypted-content affinity routing instead of duplicating the walk. Uncalled pending_days is removed from daily global spend rollup. _PodLockManager is dropped in favor of typed proxy_logging_obj.db_spend_update_writer.pod_lock_manager.

Suppressions and style: Inert or mis-placed mutable-ok/rebind-ok/noqa: BLE001/pyright trailers are removed or moved onto the lines gates actually report; guardrail and similar comments are trimmed where they no longer justify anything. Anthropic streaming refusal handling rebuilds the chunk via typed MessageDelta/MessageBlockDelta spreads instead of cast.

Reviewed by Cursor Bugbot for commit 018a6a9. Bugbot is set up for automated code reviews on this repo. Configure here.

Link to Devin session: https://app.devin.ai/sessions/c477ad87a9ea40fcad90d6aadff1e3a4
Open in Devin Desktop: https://app.devin.ai/desktop/session/c477ad87a9ea40fcad90d6aadff1e3a4?variant=devin

…026-09-06)

Drop the TID251 cast import and both cast-ok casts from the refusal
message_delta rebuild by narrowing the TypedDict union on its type literal,
drop the redundant Mapping cast after the isinstance check in
_mapping_field, and type the Lyria predict read-only helpers as
Mapping[str, object] instead of a bare dict with mutable-ok.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding findings or new actionable issues

Summary

This PR tightens typing, removes redundant suppressions and attribute probes, and shares the Anthropic content traversal helper

  • The latest change fully types Vercel embedding error headers without changing error handling
  • MCP pagination retains its non-string cursor guard
  • No new actionable issues were identified, and neither previous finding remains outstanding

Reviews (10) · Last reviewed commit: "refactor(vercel_ai_gateway): type the em..."

@codspeed

codspeed Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_techdebt_20260906 (018a6a9) with main (1a71454)

Open in CodSpeed

@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

mateo-berri and others added 2 commits September 9, 2026 07:47
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title refactor: clear fresh tech debt from the last 24 hours (2026-09-05, 2026-09-06) refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09) Sep 9, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

mateo-berri and others added 3 commits September 10, 2026 07:44
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…read out of the cleanup

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09) refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10) Sep 10, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

@yuneng-berri
yuneng-berri deleted the branch main September 13, 2026 04:50
@mateo-berri mateo-berri reopened this Sep 13, 2026
@mateo-berri
mateo-berri changed the base branch from litellm_internal_staging to main September 13, 2026 05:15
mateo-berri and others added 2 commits September 13, 2026 07:45
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10) refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13) Sep 13, 2026
@devin-ai-integration
devin-ai-integration Bot changed the base branch from main to litellm_internal_staging September 13, 2026 08:06
@cursor

cursor Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

…0906

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	litellm/rust_bridge/legacy_callbacks.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… the cleanup

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13, 2026-09-16, 2026-09-17, 2026-09-18) refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13, 2026-09-16, 2026-09-17, 2026-09-18, 2026-09-19, 2026-09-20) Sep 20, 2026
…0906

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	litellm/router_strategy/complexity_router/jev_classifier.py
#	litellm/types/utils.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot changed the title refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13, 2026-09-16, 2026-09-17, 2026-09-18, 2026-09-19, 2026-09-20) refactor: clear fresh tech debt from the last 24 hours (2026-09-06, 2026-09-09, 2026-09-10, 2026-09-13, 2026-09-16, 2026-09-17, 2026-09-18, 2026-09-19, 2026-09-20, 2026-09-22) Sep 22, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

Comment thread litellm/llms/vercel_ai_gateway/embedding/transformation.py Outdated
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 018a6a9. Configure here.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Superseded by #42710, which now carries this cleanup: every run from 2026-09-06 to 2026-09-22 was merged onto that rolling branch

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 018a6a90 Waiting Sep 22, 2026 by devin-ai-integration[bot] via oauth #353
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.

3 participants