Repository navigation
refactor(types): replace Any with proven types in 7 files - #43844
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
…ools, jwt routing and complexity router paths Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ 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 85baad6. Configure here.
TLDR
Problem this solves:
Anysilently disables type checking wherever it landsAnyerrors across the backend treeHow it solves it:
Anywith proven types in 7 filesAnyerrors, 14 basedpyright errors totalUser Flow
This change only replaces
Anyannotations with proven types in seven files, so a developer calling the proxy sees the same flow at the merge base and at the tip, observed live against a 2-worker proxy on each commitBefore (merge base): every call a developer makes behaves the same as it does on main today
POST https://litellm-domain/v1/chat/completionswithqa-geminiand "Reply with exactly PONG" returns 200 withPONG,finish_reason=stopand non-zero prompt and completion tokensPOST https://litellm-domain/v1/chat/completionswithqa-openaiand"stream": truereturns 200 as SSE chunks assembling the model's short reply with a final usage chunkPOST https://litellm-domain/v1/embeddingswithqa-embedreturns 200 with one 3072-dimension vector and 5 prompt tokensPOST https://litellm-domain/key/generatewith the master key returns a freshsk-...virtual key limited toqa-geminiPOST https://litellm-domain/v1/chat/completionswith that key andqa-geminireturns 200 withOKqa-openaireturns 403GET https://litellm-domain/key/info?key=sk-...shows spend above 0 for the keyPOST https://litellm-domain/key/deletefor that key returns 200, and the next chat call with it returns 401GET https://litellm-domain/guardrails/listreturns 200 with an emptyguardrailslistGET https://litellm-domain/v1/modelsreturns 200 listingqa-embed,qa-fireworks,qa-geminiandqa-openaiPOST https://litellm-domain/v1/responseswithqa-gemini, streaming and not, returns 200 withPONG(9 events fromresponse.createdtoresponse.completedwhen streaming), and withqa-openaireturns 200 withPONGand non-zero usagePOST https://litellm-domain/v1/chat/completionswithqa-fireworksreturns 412 because the provider account is suspendedPOST https://litellm-domain/v1/chat/completionswithqa-geminiand aget_weatherJSON-schema tool returns 200 with one tool call, arguments{"city": "Paris"},finish_reason=tool_callsAfter (this PR): the same developer walking the same 13 steps sees the same status codes and response shapes
POST https://litellm-domain/v1/chat/completionswithqa-geminiand "Reply with exactly PONG" returns 200 withPONG,finish_reason=stopand non-zero prompt and completion tokensPOST https://litellm-domain/v1/chat/completionswithqa-openaiand"stream": truereturns 200 as SSE chunks assembling the model's short reply with a final usage chunkPOST https://litellm-domain/v1/embeddingswithqa-embedreturns 200 with one 3072-dimension vector and 5 prompt tokensPOST https://litellm-domain/key/generatewith the master key returns a freshsk-...virtual key limited toqa-geminiPOST https://litellm-domain/v1/chat/completionswith that key andqa-geminireturns 200 withOKqa-openaireturns 403GET https://litellm-domain/key/info?key=sk-...shows spend above 0 for the keyPOST https://litellm-domain/key/deletefor that key returns 200, and the next chat call with it returns 401GET https://litellm-domain/guardrails/listreturns 200 with an emptyguardrailslistGET https://litellm-domain/v1/modelsreturns 200 listingqa-embed,qa-fireworks,qa-geminiandqa-openaiPOST https://litellm-domain/v1/responseswithqa-gemini, streaming and not, returns 200 withPONG(9 events fromresponse.createdtoresponse.completedwhen streaming), and withqa-openaireturns 200 withPONGand non-zero usagePOST https://litellm-domain/v1/chat/completionswithqa-fireworksreturns 412 because the provider account is suspendedPOST https://litellm-domain/v1/chat/completionswithqa-geminiand aget_weatherJSON-schema tool returns 200 with one tool call, arguments{"city": "Paris"},finish_reason=tool_callsPre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito 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
Shared setup for both runs. Same config, same payloads, same live providers, same Postgres; only the checked-out commit differs
Before (62a1b7f)
Type-check count
Run basedpyright over the tree the way
scripts/type_check_gate.pydoes:Observed:
Chat completions
Send a non-streaming chat completion to
qa-gemini:Observed:
Send a streaming chat completion with usage to
qa-openai:Observed:
Send a non-streaming chat completion to
qa-fireworks:Observed:
Tool calling
Send a chat completion to
qa-geminicarrying aget_weatherfunction tool:Observed:
Responses API
Send a non-streaming responses request to
qa-gemini:Observed:
Send a non-streaming responses request to
qa-openai:Observed:
Send a streaming responses request to
qa-gemini:Observed:
Embeddings
Send an embedding request to
qa-embed:Observed:
Virtual key lifecycle, model access control and spend
Generate a key scoped to
qa-gemini, use it on both models, read its spend, delete it and reuse it:Observed:
Guardrails and model list
List the guardrails and the models:
Observed:
After (85baad6)
Type-check count
Run basedpyright over the tree the way
scripts/type_check_gate.pydoes:Observed:
Chat completions
Send a non-streaming chat completion to
qa-gemini:Observed:
Send a streaming chat completion with usage to
qa-openai:Observed:
Send a non-streaming chat completion to
qa-fireworks:Observed:
Tool calling
Send a chat completion to
qa-geminicarrying aget_weatherfunction tool:Observed:
Responses API
Send a non-streaming responses request to
qa-gemini:Observed:
Send a non-streaming responses request to
qa-openai:Observed:
Send a streaming responses request to
qa-gemini:Observed:
Embeddings
Send an embedding request to
qa-embed:Observed:
Virtual key lifecycle, model access control and spend
Generate a key scoped to
qa-gemini, use it on both models, read its spend, delete it and reuse it:Observed:
Guardrails and model list
List the guardrails and the models:
Observed:
Live A/B on changed paths (real Gemini and OpenAI, 2 proxy workers, fresh Postgres per leg)
Same config and probes on both legs: a complexity router with a live Gemini judge, a Responses API call mixing a gateway MCP tool (a local MCP math server behind a
pre_mcp_callcontent-filter guardrail) with a caller-ownedlookup_weatherfunction tool, OpenAI chat streamed with usage, a response-cache repeat, and five virtual key generate, use, delete and reuse cycles. OpenAI is reached through a local recorder that forwards to the real API and logs what the proxy sentsimple-tieron Geminisimple-tieron Geminicomplex-tieron OpenAIcomplex-tieron OpenAImath-add,math-multiply,math-fail,lookup_weather; MCP peer got oneadd(2, 3)add(2, 3)mcp-filterpre_mcp_callsuccessrowmcp-filterpre_mcp_callsuccessrow[DONE][DONE]cache_hitFalse, True, Truecache_hitFalse, True, Truecache_hit=True)cache_hit=True)The follow-up model turn after the MCP call ended in a text message at the base and in a
lookup_weathercall at the head; the proxy's request to OpenAI for that turn differs only byprevious_response_id, so the difference is the live model's choiceChanged symbols and every caller, from
rgoverlitellm/andenterprise/:Logging.handle_sync_success_callbacks_for_async_calls(callers inutils.py,caching/caching_handler.pytwice,files/streaming.py,rust_bridge/callbacks_legacy_python.py, plus theLoggingSurfaceprotocol),CustomStreamWrapper._dispatch_provider_chunk(one caller in the same class),_record_mcp_guardrail_evaluations(two callers inmcp_server_manager.py),_get_source_cache_base_spend(one caller inproxy_server.py),LiteLLM_Proxy_MCP_Handler._parse_mcp_tools(localother_toolsonly) andComplexityRouter._classify_with_llm(one caller, no subclass). No registry, config key, DB column or wire field changes.litellm.utils.executor is thread_pool_executor.executor is litellm_logging.executorprintsTrueat the headLive A/B rig: config, probe script and raw output per leg
Each leg runs
bash leg.sh <base|head> <checkout>: a fresh Postgres database, an OpenAI recorder that forwards to api.openai.com, a local MCP math peer that logs everytools/call, thenlitellm --config config.yaml --num_workers 2andpython probes.py. Base checkout is 62a1b7f, head is 85baad6config.yaml (ports templated per leg)
custom_callbacks.py
probes.py
Checkout each leg's proxy imported, printed by
leg.shbefore starting itEvery request the proxy sent to OpenAI, from the recorder in front of api.openai.com, compared between legs with response and call ids masked
probes.py stdout at 62a1b7f
probes.py stdout at 85baad6
MCP peer log of
tools/callrequests, 62a1b7f then 85baad6LiteLLM_SpendLogsguardrail column at 85baad6 (62a1b7f matches apart from ids and timestamps)select call_type, model_group, status, has_guardrail from "LiteLLM_SpendLogs"is byte-identical between the legs (diffexits 0)Sync success callback path on async calls: SDK probe with a real Gemini and OpenAI call per leg
The only runtime change is where
litellm_logging.pyimportsexecutorfrom. This probe sets a function inlitellm.success_callbackand aCustomLogger, wrapshandle_sync_success_callbacks_for_async_callsand the sync success handler body to record each hit and its thread, and makes twoacompletioncalls togemini/gemini-2.5-flash-liteand one streamedacompletiontogpt-4o-mini. On both legs every call is dispatched withshould_run=Trueand the sync success handler runs on thelitellm-logging_0executor thread, soexecutor.submitran live. The plain function insuccess_callbackdoes not fire on either leg because the handler body skips it for async requests, which is unchanged behaviorCheckout each SDK leg imported
62a1b7f
{ "litellm": "~/any_sweep_20261002/basewt_tests/litellm/__init__.py", "rows": [ { "call": "acompletion-0", "content": "PONG", "cache_hit": null }, { "call": "acompletion-1", "content": "PONG", "cache_hit": null }, { "call": "acompletion-stream", "content": "4 5 6" } ], "marks": [ { "kind": "dispatch", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": null, "stream": false }, { "kind": "dispatch", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": null, "stream": false }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": false, "stream": true }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": false, "stream": true, "should_run": true } ] }85baad6
{ "litellm": "~/repos/litellm/litellm/__init__.py", "rows": [ { "call": "acompletion-0", "content": "PONG", "cache_hit": null }, { "call": "acompletion-1", "content": "PONG", "cache_hit": null }, { "call": "acompletion-stream", "content": "1, 2, 3." } ], "marks": [ { "kind": "dispatch", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": null, "stream": false }, { "kind": "dispatch", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": null, "stream": false }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": null, "stream": false, "should_run": true }, { "kind": "async_logger", "call_type": "acompletion", "cache_hit": false, "stream": true }, { "kind": "success_handler", "thread": "litellm-logging_0", "call_type": "acompletion", "cache_hit": false, "stream": true, "should_run": true } ] }Fireworks raw response
The
qa-fireworksstep returns 412 on both legs and the harness prints its ownKeyError: 'choices'instead of the body. The same request sent straight to Fireworks with the same key returnsNew integration tests run locally at 85baad6
These run in CircleCI's
integration-mcpandintegration-providersjobs, which are not GitHub checks on this PR, so they were run locally the way.circleci/scripts/run_integration.shruns them: a fresh Postgres database, Redis,integration._support.upstream, twointegration._support.proxyprocesses on 4000 and 4001, thentests/integration/run.py85baad6 passes /live-pr-risk
85baad6 passes /audit (skipped: no product code on a request path, a refactor: annotations plus a same-object import move)
Type
🧹 Refactoring
Caveats (if any)
Low
SplitToolsstill types the second list aslist[Any], so callers of_parse_mcp_toolsstill seeAnythere even thoughother_toolsis nowlist[ToolParam]. Left as is: narrowing the alias needs a new product commit reaching every consumer of that tuple and a full rerun of every live proof for one alias the next sweep can takecache_hittobool | Noneon theLoggingSurfaceprotocol is type-only: every caller passesTrue, abool,Literal[True] | Noneor nothing, andsuccess_handleralready takesbool | Nonesuccess_callbackdoes not fire on async calls on either leg, because the sync handler skips it for async requests. The changedexecutor.submitpath itself ran on both legs (SDK probe above), so this is unchanged behaviorFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/aa528093d62148a18c529112c3bc472f
Open in Devin Desktop: https://app.devin.ai/desktop/session/aa528093d62148a18c529112c3bc472f?variant=devin