Skip to content

Commit 1204e63

Browse files
ayaangazaliclaude
andcommitted
[perf][imports][defer litellm and mcp so import swarms does no provider work]
import swarms eagerly imported litellm (~1.1s, and litellm's own __init__ fetches its model-cost map from raw.githubusercontent.com) and mcp, neither of which is needed until the first LLM call or the first MCP use. Import drops from ~2.2s to ~0.6s and no longer touches the network — verified by importing with socket.connect monkeypatched to raise. litellm deferral, per module: - litellm_wrapper binds litellm/completion/supports_vision into module globals on first LiteLLM construction. Module attributes rather than function-local imports so existing tests that patch litellm_wrapper.completion keep working; each name binds only while None, so an active patch is never overwritten. context_compressor gets the same treatment for its patched completion. - agent.py's reasoning_effort annotation called get_reasoning_efforts() in the class body, which introspects litellm.completion — forcing the import at class-definition time. The Literal on a plain class is never enforced at runtime, so it now uses the static REASONING_EFFORTS set the same module already maintains for exactly this fallback. - litellm_tokenizer, agent_loader, agent_router, tree_swarm, llm_manager and agent.py move their litellm imports into the functions that use them (count_tokens, model validation, embedding calls, capability checks, reliability_check). - The litellm exception names vanish from except tuples that already contained Exception, which subsumed them; behaviour is identical and the exceptions still propagate to callers. mcp deferral: mcp_manager and aop gain `from __future__ import annotations` so mcp types in signatures stop needing the real classes at def time, TYPE_CHECKING imports keep the annotations meaningful, and the two runtime uses (ClientSession construction, the MCPTool isinstance check, FastMCP construction) import locally. Two subprocess regression tests in tests/test___init__.py pin the contract: import swarms leaves litellm/mcp/openai unloaded, and the first LiteLLM construction still binds litellm. Addresses kyegomez#1754 and the import-time side effects behind kyegomez#1739 and kyegomez#1738. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 16afc75 commit 1204e63

11 files changed

Lines changed: 174 additions & 52 deletions

File tree

swarms/agents/context_compressor.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
from typing import Any, Optional
1111

12-
from litellm import completion
1312
from loguru import logger
1413

1514
from swarms.utils.litellm_tokenizer import count_tokens
1615

16+
# Bound on first use; see summarize(). Patched directly by tests.
17+
completion = None
18+
1719

1820
COMPRESSION_SYSTEM_PROMPT = """
1921
You are a conversation compression expert. Your job is to produce a faithful, dense summary of an ongoing agent conversation so that the agent can continue its work without losing critical context.
@@ -98,6 +100,16 @@ def _summarize(self, agent: Any, history: str) -> str:
98100
raise ValueError(
99101
"No summarizer_model configured and agent has no model_name"
100102
)
103+
# Deferred: litellm must not load at `import swarms` time
104+
# (#1754). Bound into the module global so tests can keep patching
105+
# ``swarms.agents.context_compressor.completion``; an active patch
106+
# is non-None and is never overwritten.
107+
global completion
108+
if completion is None:
109+
from litellm import completion as _completion
110+
111+
completion = _completion
112+
101113
response = completion(
102114
model=model,
103115
messages=[

swarms/agents/llm_manager.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,6 @@
3131
import traceback
3232
from typing import Any, Callable, List, Optional, Union
3333

34-
from litellm.exceptions import (
35-
AuthenticationError,
36-
BadRequestError,
37-
InternalServerError,
38-
)
39-
from litellm.utils import (
40-
supports_function_calling,
41-
supports_parallel_function_calling,
42-
supports_vision,
43-
)
4434
from loguru import logger
4535

4636
from swarms.schemas.agent_errors import AgentLLMInitializationError
@@ -325,6 +315,13 @@ def check_model_supports_utilities(
325315
"""
326316
agent = self.agent
327317

318+
# Deferred: litellm must not load at `import swarms` time (#1754).
319+
from litellm.utils import (
320+
supports_function_calling,
321+
supports_parallel_function_calling,
322+
supports_vision,
323+
)
324+
328325
# Only check vision support if an image is provided
329326
if img is not None:
330327
out = supports_vision(agent.model_name)
@@ -549,12 +546,7 @@ def call(
549546

550547
return agent.llm.run(**run_args, **kwargs)
551548

552-
except (
553-
BadRequestError,
554-
InternalServerError,
555-
AuthenticationError,
556-
Exception,
557-
) as e:
549+
except Exception as e:
558550
logger.error(
559551
f"Error calling LLM with model '{self.get_current_model()}': {e}. "
560552
f"Task: {task}, Args: {args}, Kwargs: {kwargs} Traceback: {traceback.format_exc()}"

swarms/structs/agent.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,6 @@
1919

2020
import toml
2121
import yaml
22-
from litellm import model_list
23-
from litellm.exceptions import (
24-
AuthenticationError,
25-
BadRequestError,
26-
InternalServerError,
27-
)
28-
from litellm.utils import (
29-
get_max_tokens,
30-
get_model_info,
31-
supports_function_calling,
32-
)
3322
from loguru import logger
3423
from pydantic import BaseModel
3524

@@ -50,7 +39,7 @@
5039
AgentToolError,
5140
AgentToolExecutionError,
5241
)
53-
from swarms.utils.get_reasoning_efforts import get_reasoning_efforts
42+
from swarms.utils.get_reasoning_efforts import REASONING_EFFORTS
5443
from swarms.utils.workspace_utils import get_workspace_dir
5544
from swarms.artifacts.main_artifact import Artifact
5645
from swarms.prompts.agent_system_prompts import AGENT_SYSTEM_PROMPT_3
@@ -411,7 +400,7 @@ def __init__(
411400
reasoning_prompt_on: bool = True,
412401
dynamic_context_window: bool = True,
413402
show_tool_execution_output: bool = True,
414-
reasoning_effort: Literal[get_reasoning_efforts()] = "medium",
403+
reasoning_effort: Literal[REASONING_EFFORTS] = "medium",
415404
thinking_tokens: int = 1024,
416405
reasoning_enabled: bool = False,
417406
handoffs: Optional[Union[Sequence[Callable], Any]] = None,
@@ -1601,12 +1590,7 @@ def _run(
16011590
loop_count=loop_count
16021591
)
16031592

1604-
except (
1605-
BadRequestError,
1606-
InternalServerError,
1607-
AuthenticationError,
1608-
Exception,
1609-
) as e:
1593+
except Exception as e:
16101594

16111595
# Track the LLM/generation error via telemetry — the
16121596
# retry loop swallows it, so capture_run never sees it.
@@ -3075,6 +3059,8 @@ def _default_context_length(self) -> int:
30753059
which may not correspond to available context for input (e.g., 32768 for gpt-4.1 output, but
30763060
over a million for certain input windows).
30773061
"""
3062+
from litellm.utils import get_model_info
3063+
30783064
try:
30793065
return (
30803066
get_model_info(self.model_name).get(
@@ -3096,6 +3082,8 @@ def _default_max_tokens(self) -> int:
30963082
Returns:
30973083
int: The maximum number of output tokens for the model. Returns 16000 if undetermined.
30983084
"""
3085+
from litellm.utils import get_model_info
3086+
30993087
# get_model_info raises for an unmapped model id rather than
31003088
# returning an empty mapping, so an unknown, custom or self-hosted
31013089
# model would otherwise take down Agent.__init__. The sibling
@@ -3127,6 +3115,14 @@ def reliability_check(self):
31273115
"Max loops is not provided or is set to 0. Please set max loops to 1 or more."
31283116
)
31293117

3118+
# Deferred: litellm costs >1s to import and fetches its model-cost
3119+
# map, so it must not load at `import swarms` time (#1754, #1739).
3120+
from litellm import model_list
3121+
from litellm.utils import (
3122+
get_max_tokens,
3123+
supports_function_calling,
3124+
)
3125+
31303126
# Ensure max_tokens is set to a valid value based on the model, with a robust fallback.
31313127
if self.max_tokens is None or self.max_tokens <= 0:
31323128
suggested_tokens = get_max_tokens(self.model_name)
@@ -4106,9 +4102,6 @@ def run(
41064102
except (
41074103
AgentRunError,
41084104
AgentLLMError,
4109-
BadRequestError,
4110-
InternalServerError,
4111-
AuthenticationError,
41124105
Exception,
41134106
) as e:
41144107

swarms/structs/agent_loader.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
)
1616

1717
import yaml
18-
from litellm import model_list
1918
from tqdm import tqdm
2019

2120
from swarms.agents.create_agents_from_yaml import (
@@ -110,7 +109,11 @@ def validate_config(
110109
if isinstance(config, AgentSpec):
111110
config = config.model_dump()
112111

113-
# Validate model name using litellm model list
112+
# Validate model name using litellm model list.
113+
# Deferred import: litellm must not load at `import swarms`
114+
# time (#1754).
115+
from litellm import model_list
116+
114117
model_name = str(config["model_name"])
115118
# model_list from litellm is a list of strings, not dicts
116119
if isinstance(model_list, list) and len(model_list) > 0:

swarms/structs/agent_router.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import math
22
from typing import Any, Callable, List, Optional, Union
33

4-
from litellm import embedding
54
from tenacity import retry, stop_after_attempt, wait_exponential
65

76
from swarms.structs.ma_blocks import find_agent_by_name
@@ -57,6 +56,9 @@ def _generate_embedding(self, text: str) -> List[float]:
5756
List[float]: The embedding vector as a list of floats.
5857
"""
5958
try:
59+
# Deferred: litellm must not load at `import swarms` time (#1754).
60+
from litellm import embedding
61+
6062
# Prepare parameters for the embedding call
6163
params = {"model": self.embedding_model, "input": [text]}
6264

swarms/structs/aop.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
from contextlib import AbstractAsyncContextManager
35
import socket
@@ -12,10 +14,17 @@
1214
from uuid import uuid4
1315

1416
from loguru import logger
15-
from mcp.server.auth.settings import AuthSettings
16-
from mcp.server.fastmcp import FastMCP
17-
from mcp.server.lowlevel.server import LifespanResultT
18-
from mcp.server.transport_security import TransportSecuritySettings
17+
from typing import TYPE_CHECKING
18+
19+
if (
20+
TYPE_CHECKING
21+
): # Deferred: mcp must not load at `import swarms` (#1754)
22+
from mcp.server.auth.settings import AuthSettings
23+
from mcp.server.fastmcp import FastMCP
24+
from mcp.server.lowlevel.server import LifespanResultT
25+
from mcp.server.transport_security import (
26+
TransportSecuritySettings,
27+
)
1928

2029
from swarms.structs.agent import Agent
2130
from swarms.structs.omni_agent_types import AgentType
@@ -679,6 +688,9 @@ def __init__(
679688
self.task_queues: Dict[str, TaskQueue] = {}
680689
self.transport = transport
681690

691+
# Deferred: mcp must not load at `import swarms` time (#1754).
692+
from mcp.server.fastmcp import FastMCP
693+
682694
self.mcp_server = FastMCP(
683695
name=server_name,
684696
port=port,

swarms/structs/tree_swarm.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from datetime import datetime, timezone
44
from typing import Any, List, Optional
55

6-
from litellm import embedding
76
from pydantic import BaseModel, Field
87

98
from swarms.structs.agent import Agent
@@ -194,6 +193,9 @@ def _get_embedding(self, text: str) -> List[float]:
194193
List[float]: Embedding vector
195194
"""
196195
try:
196+
# Deferred: litellm must not load at `import swarms` time (#1754).
197+
from litellm import embedding
198+
197199
response = embedding(
198200
model=self.embedding_model_name, input=[text]
199201
)

swarms/tools/mcp_manager.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
``MCPManager`` is the only object an ``Agent`` needs to hold.
1717
"""
1818

19+
from __future__ import annotations
1920
import asyncio
2021
import json
2122
import os
@@ -32,8 +33,13 @@
3233
from urllib.parse import parse_qs, urlparse
3334

3435
from loguru import logger
35-
from mcp import ClientSession
36-
from mcp.types import Tool as MCPTool
36+
from typing import TYPE_CHECKING
37+
38+
if (
39+
TYPE_CHECKING
40+
): # Deferred: mcp must not load at `import swarms` (#1754)
41+
from mcp import ClientSession
42+
from mcp.types import Tool as MCPTool
3743

3844
from swarms.schemas.agent_mcp_errors import (
3945
AgentMCPConnectionError,
@@ -877,6 +883,8 @@ def _tool_name(tool: Any) -> Optional[str]:
877883
if isinstance(tool, dict):
878884
function = tool.get("function") or {}
879885
return function.get("name") or tool.get("name")
886+
from mcp.types import Tool as MCPTool
887+
880888
if isinstance(tool, MCPTool):
881889
return tool.name
882890
return getattr(tool, "name", None)
@@ -1493,6 +1501,8 @@ async def _session(self, connection: MCPConnection):
14931501
)
14941502

14951503
try:
1504+
from mcp import ClientSession
1505+
14961506
async with client_cm as ctx:
14971507
read, write = ctx[0], ctx[1]
14981508
async with ClientSession(

swarms/utils/litellm_tokenizer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from litellm import encode, model_list
21
from loguru import logger
32
from typing import Optional
43
from functools import lru_cache
@@ -30,6 +29,9 @@ def count_tokens(
3029
logger.warning("Empty or whitespace-only text provided")
3130
return 0
3231

32+
# Deferred: litellm must not load at `import swarms` time (#1754).
33+
from litellm import encode
34+
3335
# Set fallback encoder
3436
fallback_model = default_encoder or DEFAULT_MODEL
3537

@@ -74,6 +76,8 @@ def count_tokens(
7476
def get_supported_models() -> list:
7577
"""Get list of supported models from litellm."""
7678
try:
79+
from litellm import model_list
80+
7781
return model_list
7882
except Exception as e:
7983
logger.warning(f"Could not retrieve model list: {e}")

swarms/utils/litellm_wrapper.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424
from pathlib import Path
2525
from typing import List, Optional
2626

27-
import litellm
2827
import requests
29-
from litellm import completion, supports_vision
3028
from loguru import logger
3129
from pydantic import BaseModel
3230

@@ -38,6 +36,37 @@
3836
)
3937

4038

39+
# Bound by _bind_litellm() on first LiteLLM construction.
40+
litellm = None
41+
completion = None
42+
supports_vision = None
43+
44+
45+
def _bind_litellm() -> None:
46+
"""Import litellm on first LiteLLM construction, not at module import.
47+
48+
Importing litellm costs >1s and fetches its model-cost map from the
49+
network, so it must not run at `import swarms` time (#1754, #1739).
50+
The callables are bound into this module's globals so every existing
51+
call site — and every test that patches ``litellm_wrapper.completion``
52+
as a module attribute — keeps working unchanged. Each name binds only
53+
while None, so an active patch is never overwritten.
54+
"""
55+
global litellm, completion, supports_vision
56+
if litellm is None:
57+
import litellm as _litellm
58+
59+
litellm = _litellm
60+
if completion is None:
61+
from litellm import completion as _completion
62+
63+
completion = _completion
64+
if supports_vision is None:
65+
from litellm import supports_vision as _supports_vision
66+
67+
supports_vision = _supports_vision
68+
69+
4170
class LiteLLMException(Exception):
4271
"""
4372
Custom exception raised for LiteLLM-specific errors.
@@ -339,6 +368,10 @@ def __init__(
339368
self.modalities = []
340369
self.messages = [] # Initialize messages list
341370

371+
# First construction pays the litellm import; `import swarms` no
372+
# longer does.
373+
_bind_litellm()
374+
342375
# Configure litellm settings
343376
litellm.set_verbose = (
344377
verbose # Disable verbose mode for better performance

0 commit comments

Comments
 (0)