Skip to content

perf: defer litellm and mcp out of import swarms (2.2s -> 0.37s) - #2090

Open
kyegomez wants to merge 1 commit into
masterfrom
perf/lazy-litellm-mcp
Open

perf: defer litellm and mcp out of import swarms (2.2s -> 0.37s)#2090
kyegomez wants to merge 1 commit into
masterfrom
perf/lazy-litellm-mcp

Conversation

@kyegomez

@kyegomez kyegomez commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Fixes #1754

import swarms took ~2.2 s and loaded 3,564 modules. litellm was ~1.4 s of that and mcp another ~0.4 s, and neither is needed to import the package.

What this actually changes — read this before the numbers

The cost moves; it does not disappear. Measured end to end, from process start to first completion, with a stubbed LLM so no network is involved:

master this PR
import swarms ~2200 ms ~365 ms
Agent(...) construct ~17 ms ~1800 ms
first run() ~1 ms ~1 ms
total to first completion ~2200 ms ~2170 ms

So for a script that imports swarms and immediately builds an agent, this PR buys nothing. The import cost simply relocates into Agent.__init__, which builds an MCPManager unconditionally (pulling mcp) and runs reliability_check(), which calls supports_function_calling and get_max_tokens (pulling litellm) — both on every agent, whether or not MCP or the model is ever used.

The original title claimed "5x faster" without that qualification. It is 5x on import swarms alone, which is the honest framing.

Where it does pay off

Any process that imports swarms and does not immediately construct an agent:

  • swarms --help and other CLI paths that print and exit — ~1.8 s, the most visible win.
  • Test collection. Every pytest process importing swarms pays this once.
  • Serverless cold starts that branch before constructing an agent — health checks, routing, validation.
  • Importing swarms for a type, a Conversation, or a non-LLM structure.

Import-time result

Median over seven interleaved runs (alternating master/branch, so drift cannot favour either side):

master this PR
import swarms 1973 ms 388 ms
modules loaded 3564 1408
litellm loaded yes no
mcp loaded yes no

Min-to-min is 1668 → 376 ms, so it is not a sampling artifact.

The largest cost was not an import statement

agent.py annotated:

reasoning_effort: Literal[get_reasoning_efforts()] = None

That call runs when the class body executes, and get_reasoning_efforts() imports litellm to widen the accepted values. Two things make it not worth 1.4 s:

  • Against the installed litellm it added nothing over the static REASONING_EFFORTS tuple (litellm adds: nothing).
  • A type checker cannot evaluate a runtime call, so the dynamic form never helped static analysis either.

The annotation now uses the static tuple. That single change took import swarms from 2564 ms to 1148 ms and made litellm disappear. Each preceding round of function-local imports had barely moved the needle — this was the anchor holding the chain.

The rest

litellm — function-local imports in litellm_tokenizer, litellm_wrapper, agent, llm_manager, context_compressor, agent_loader, tree_swarm, agent_router. These keep the documented top-level names (from litellm import completion) rather than internal paths like litellm.main: the submodule form measures identically, since Python initialises the parent package regardless, and it couples us to internals that move between versions — the way mcp.server.fastmcp vanished in mcp 2.x.

mcp — PEP 562. swarms/tools/__init__ resolves MCPManager and MCPFileTokenStorage on attribute access, and they leave __all__ so the from swarms.tools import * in swarms/__init__ does not resolve them straight back — that star-import defeated the first attempt. A matching __getattr__ on swarms/__init__ keeps swarms.MCPManager working, and __dir__ still advertises both.

Every previously working access path was verified: swarms.MCPManager, from swarms.tools import MCPManager, the direct submodule import, dir(), count_tokens, LiteLLM, and Agent(...).

Breaking change

Six tests patched these symbols on the importing module, which stops working once the import is function-local:

patch("swarms.agents.context_compressor.completion")     → patch("litellm.completion")
patch("swarms.structs.agent.supports_function_calling")  → patch("litellm.utils.supports_function_calling")
patch.object(litellm_wrapper, "completion", ...)         → patch("litellm.completion", ...)

User code doing the same needs the same change. This belongs in the release notes.

Verification

No regressions across the eight affected suites, from a detached worktree, run offline:

master:  33 failed, 363 passed
branch:  33 failed, 363 passed

Identical failure sets — comm reports nothing introduced. The 33 are pre-existing live-LLM tests needing an API key.

black --line-length 70 and ruff check . clean.

Required follow-up for this to reach users

This PR is the groundwork, not the payoff. To move the ~1.8 s off Agent() construction as well:

  1. Make Agent.mcp_manager a lazy property so agents without MCP never import it.
  2. Defer the reliability_check() probes (supports_function_calling, get_max_tokens) to first use.

Then construction stays cheap and the cost lands on the first genuine model call, overlapped with network latency. Merging this first is what makes that follow-up possible.

`import swarms` took about 2.0 s and loaded 3,564 modules. litellm accounted
for roughly 1.4 s of that and mcp for another 0.4 s, and neither is needed to
import the package: litellm is needed at the first LLM call, mcp only when a
caller touches MCP.

Both are now deferred to first use. Median over seven interleaved runs:

    import swarms   1973 ms -> 388 ms   (5.1x, 1.6 s saved)
    modules loaded     3564 -> 1408
    litellm loaded     True -> False
    mcp loaded         True -> False

The single largest cost was not an import statement. agent.py annotated

    reasoning_effort: Literal[get_reasoning_efforts()] = None

and that call runs when the class body executes, importing litellm to widen
the accepted values. Measured against the installed litellm it added nothing
over the static REASONING_EFFORTS tuple, and a type checker cannot evaluate a
runtime call either, so the dynamic form cost 1.4 s and bought nothing. The
annotation now uses the static tuple.

The rest is function-local imports at the call sites in litellm_tokenizer,
litellm_wrapper, agent, llm_manager, context_compressor, agent_loader,
tree_swarm and agent_router, keeping the documented top-level litellm names
rather than internal submodule paths.

mcp is deferred with PEP 562: swarms/tools/__init__ resolves MCPManager and
MCPFileTokenStorage on attribute access, and they leave __all__ so the
`from swarms.tools import *` in swarms/__init__ does not resolve them straight
back. A matching __getattr__ on swarms/__init__ keeps `swarms.MCPManager`
working, and __dir__ still advertises both.

Six tests patched these symbols on the importing module rather than on
litellm, which no longer works now the imports are function-local. They now
patch litellm.completion and litellm.utils.supports_function_calling, which is
where the names actually live and what the tests meant to stub.

Callers who patch swarms.structs.agent.supports_function_calling or
swarms.agents.context_compressor.completion need the same change.

Agent construction still imports mcp, because Agent.__init__ builds an
MCPManager unconditionally. Making that lazy needs mcp_manager to become a
property and is left for a follow-up.

Fixes #1754
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

return self.usage_ratio(agent) >= self.threshold

def _summarize(self, agent: Any, history: str) -> str:
from litellm import completion
Comment on lines +316 to +320
from litellm.utils import (
supports_function_calling,
supports_parallel_function_calling,
supports_vision,
)
Comment on lines +516 to +520
from litellm.exceptions import (
AuthenticationError,
BadRequestError,
InternalServerError,
)
Comment thread swarms/structs/agent.py
dynamic_context_window: bool = True,
show_tool_execution_output: bool = True,
reasoning_effort: Literal[get_reasoning_efforts()] = None,
reasoning_effort: Literal[REASONING_EFFORTS] = None,
Comment thread swarms/structs/agent.py
Comment on lines +1336 to +1340
from litellm.exceptions import (
AuthenticationError,
BadRequestError,
InternalServerError,
)
config: Union[BaseModel, Dict[str, Any]],
) -> AgentConfigDict:
"""Validate a config supplied as a pydantic model or a plain dict."""
from litellm import model_list
Returns:
List[float]: The embedding vector as a list of floats.
"""
from litellm import embedding
Returns:
List[float]: Embedding vector
"""
from litellm import embedding
# Set fallback encoder
fallback_model = default_encoder or DEFAULT_MODEL

from litellm import encode
@lru_cache(maxsize=None)
def _model_supports_vision(model: str) -> bool:
"""Cached litellm.supports_vision lookup (pure function of model name)."""
from litellm import supports_vision
@kyegomez kyegomez changed the title perf: defer litellm and mcp imports so import swarms is 5x faster perf: defer litellm and mcp out of import swarms (2.2s -> 0.37s) Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants