mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* fix(http_handler): dispose aiohttp session when finalized without a running loop AsyncHTTPHandler.__del__ can only schedule an async close when a running event loop exists at finalization time; in any other context (worker threads whose loop has closed, sync contexts, interpreter shutdown) the RuntimeError from get_running_loop() is swallowed and the underlying aiohttp ClientSession is abandoned to GC, emitting 'Unclosed client session' / 'Unclosed connector' warnings. This is the disposal gap left after the recycle-time fix: clients created for short-lived event loops (the loop-id-keyed LLM client cache mints one handler per loop) are never recycled - they live and die with their loop, and their finalization is precisely the loop-less case. Fix: - no running loop: fall back to the connector's synchronous teardown via LiteLLMAiohttpTransport._mark_connector_closed - the same finalizer-safe path used for dead-loop recycles - honoring _owns_session so a shared session is never closed. - running loop: keep the async close, but hold a strong reference to the scheduled task until it completes (a bare create_task() result may be collected before running), mirroring _background_close_tasks. Tests: loop-less finalization closes a dead-loop session; running-loop finalization registers and drains the close task; the sync fallback respects session ownership. All three fail without the fix. * lint: conform new finalizer code to the type-discipline budget Final on the five never-rebound locals (LIT010); the class-level task registry keeps its mutable set with the sanctioned mutable-ok reason, mirroring the aiohttp transport's registry (LIT001). * lint: reasoned pyright ignore on the cross-class teardown call The handler deliberately reuses the transport's finalizer-safe connector teardown; no public seam exists and an async close can never run at loop-less finalization. Clears the net-new reportPrivateUsage the basedpyright budget gate flagged once the LIT stage passed. * fix(http_handler): retrieve exceptions from finalizer close tasks A bare discard done-callback dropped the task without consuming its exception, so a failing aclose() emitted "Task exception was never retrieved" at GC, the same noise class this path exists to remove. Mirror the transport's _on_close_task_done: discard, early-return on cancellation, retrieve and debug-log the exception. * fix(http_handler): dispose foreign-loop sessions instead of scheduling aclose on the live loop GC on a live loop (e.g. the app's) of a handler whose session belongs to another, possibly dead, loop scheduled aclose() on the current loop, the cross-loop path the transport refuses. Route both that case and the loop-less case through the transport's lifecycle-aware _close_recycled_session, which picks async close on the session's own loop, threadsafe handoff, or the synchronous connector teardown. Regression test: a dead-loop session collected while another loop runs is disposed without scheduling anything on that loop. * chore: retrigger CI (test_mcp_logging payload-order flake, also failed on litellm_spendlogs_fallback_metadata minutes earlier) * test(mcp): select the MCP tool-call payload instead of the last-delivered one TestMCPLogger kept a single last-writer slot; an async success event from another call (a mocked acompletion whose log task lands late) races the MCP event for it, so the cost assertions intermittently read the wrong payload. This PR's finalizer change shifts task interleaving on the loop and tips that latent race over (also seen on an unrelated PR minutes earlier). Collect call_type=call_mcp_tool payloads in their own list and assert on those. * test(mcp): MCPLoggerHook inherits the order-independent payload capture It duplicated TestMCPLogger's init and success handler verbatim; the hook test reads the same MCP payload selection, so subclass instead. |
||
|---|---|---|
| .. | ||
| test_configs | ||
| conftest.py | ||
| mcp_e2e_upstream_server.py | ||
| mcp_server.py | ||
| test_aresponses_api_with_mcp.py | ||
| test_mcp_auth_header_extraction.py | ||
| test_mcp_auth_priority.py | ||
| test_mcp_chat_completions.py | ||
| test_mcp_client_unit.py | ||
| test_mcp_guardrails.py | ||
| test_mcp_hooks.py | ||
| test_mcp_litellm_client.py | ||
| test_mcp_logging.py | ||
| test_mcp_server.py | ||
| test_oauth2_mcp_config.yaml | ||
| test_openapi_spec_path_url.py | ||
| test_per_user_oauth_cache.py | ||
| test_proxy_mcp_e2e.py | ||
| test_semantic_tool_filter_e2e.py | ||