diff --git a/.env.example b/.env.example index c37ef194b9..bd3657a4a0 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,6 @@ SWARMS_VERBOSE_GLOBAL="False" SWARMS_API_KEY="" # Telemetry is ON by default. Set to false/0/no/off to opt out. SWARMS_TELEMETRY_ON="true" -# Library for model calls: "routehub" (installed by swarms[fast]) or "litellm". Empty uses RouteHub when installed. SWARMS_LLM_BACKEND="" # Write log files to WORKSPACE_DIR/logs. False by default (console only). SWARMS_LOG_FILES="False" diff --git a/CLAUDE.md b/CLAUDE.md index bd1b6a211b..e7537b3ded 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Set your LLM API key as an environment variable before running: export OPENAI_API_KEY="sk-..." # OpenAI / GPT models export ANTHROPIC_API_KEY="sk-ant-..." # Claude models export GROQ_API_KEY="..." # Groq -# Any provider supported by LiteLLM works +# Any provider supported by RouteHub works ``` All imports come from the top-level `swarms` package: @@ -103,7 +103,7 @@ print(result) | `agent_name` | str | `"swarm-worker-01"` | Unique name — used for memory file paths | | `agent_description` | str | generic | Shown to orchestrators for routing | | `system_prompt` | str | built-in | The agent's persona / instructions | -| `model_name` | str | `"gpt-5.4"` | Any LiteLLM model string | +| `model_name` | str | `"gpt-5.4"` | Any RouteHub model string | | `max_loops` | int \| `"auto"` | `1` | Loops before returning; `"auto"` = autonomous until done | | `tools` | list[Callable] | `None` | Python functions the agent can call | | `streaming_on` | bool | `False` | Stream tokens to stdout | @@ -140,7 +140,7 @@ result = agent.run("Research the top 5 vector databases and compare them.") ### Model names -Use any LiteLLM-compatible string: +Use any RouteHub-compatible string: ```python # OpenAI diff --git a/SKILL.md b/SKILL.md index e786156122..cc2ea0ea0f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -22,7 +22,7 @@ Swarms is a multi-agent orchestration framework. Everything is built from one pr pip install -U swarms ``` -Set the key for whichever provider you use — any [LiteLLM](https://docs.litellm.ai/docs/providers) model string works: +Set the key for whichever provider you use — any [RouteHub](https://github.com/The-Swarm-Corporation/RouteHub) model string works: ```bash export OPENAI_API_KEY="sk-..." @@ -56,7 +56,7 @@ result = agent.run("Summarize the state of the semiconductor market.") | `agent_name` | `str` | `"swarm-worker-01"` | Unique identity; keys memory + routing | | `agent_description` | `str` | generic | How orchestrators decide to route to it | | `system_prompt` | `str` | built-in | Persona and instructions | -| `model_name` | `str` | `"gpt-5.4"` | Any LiteLLM model string | +| `model_name` | `str` | `"gpt-5.4"` | Any RouteHub model string | | `max_loops` | `int \| "auto"` | `1` | Iterations, or autonomous mode | | `tools` | `list[Callable]` | `None` | Python functions the agent may call | | `temperature` | `float` | `0.5` | Sampling temperature | diff --git a/pyproject.toml b/pyproject.toml index 77b471d0d4..2a7963c202 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ python-dotenv = "*" PyYAML = "*" networkx = "*" rich = "*" -litellm = "1.76.1" +tqdm = "*" httpx = "*" requests = "*" mcp = "*" @@ -74,13 +74,12 @@ schedule = "*" opentelemetry-sdk = "*" opentelemetry-exporter-otlp-proto-http = "*" zstandard = "*" -routehub = { version = ">=0.2.0", extras = ["fast"], optional = true } +routehub = { version = ">=0.2.0", extras = ["fast"] } rustworkx = { version = "*", optional = true } [tool.poetry.extras] -# RouteHub replaces litellm for model calls when installed. # rustworkx backs GraphWorkflow(backend="rustworkx") in place of networkx. -fast = ["routehub", "rustworkx"] +fast = ["rustworkx"] [tool.poetry.scripts] swarms = "swarms.cli.main:main" diff --git a/requirements.txt b/requirements.txt index 6872e6f435..31c0c69e45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,13 +3,14 @@ loguru pydantic==2.12.5 tenacity rich +tqdm psutil python-dotenv PyYAML networkx httpx requests -litellm==1.76.1 +routehub[fast]>=0.2.0 mcp>=1.28.1,<3.0.0 schedule opentelemetry-sdk diff --git a/swarms/agents/context_compressor.py b/swarms/agents/context_compressor.py index 6a00e9fcbc..4b064dcca6 100644 --- a/swarms/agents/context_compressor.py +++ b/swarms/agents/context_compressor.py @@ -149,7 +149,7 @@ def _summarize(self, agent: Any, history: str) -> str: ], temperature=self.summarizer_temperature, max_tokens=self.summarizer_max_tokens, - # RouteHub reads this per call; litellm otherwise inherits it from the last agent built. + # RouteHub reads this per call. drop_params=True, ) call_usage = usage_from_response(response) diff --git a/swarms/agents/tree_of_thoughts.py b/swarms/agents/tree_of_thoughts.py index fd7cb26cbc..9133fac340 100644 --- a/swarms/agents/tree_of_thoughts.py +++ b/swarms/agents/tree_of_thoughts.py @@ -545,7 +545,7 @@ def __init__( name: Name used in logs, in the conversation, and as the prefix of every internal agent's name. description: Short description of the agent, for orchestrators. - model_name: Any LiteLLM model string. The model must support + model_name: Any RouteHub model string. The model must support function calling. system_prompt: System prompt for every model call. Replace it to give the search a domain persona; the step, evaluation and diff --git a/swarms/cli/main.py b/swarms/cli/main.py index 3e624c5abb..d024929e60 100644 --- a/swarms/cli/main.py +++ b/swarms/cli/main.py @@ -2091,7 +2091,7 @@ def handle_tips(args: argparse.Namespace) -> None: def handle_models(args: argparse.Namespace) -> None: """ - Discover and inspect LLM models available via LiteLLM. + Discover and inspect LLM models available via RouteHub. Flags: --search PATTERN Fuzzy-match against the model list. diff --git a/swarms/cli/models.py b/swarms/cli/models.py index ddb42bdc3e..dff457109e 100644 --- a/swarms/cli/models.py +++ b/swarms/cli/models.py @@ -9,7 +9,7 @@ - ``swarms models --info `` show context window, capabilities, pricing All metadata comes from the LLM backend's ``model_list`` and -``get_model_info``: litellm's bundled table, or OpenRouter's live list when +``get_model_info``: RouteHub's bundled table, or OpenRouter's live list when RouteHub is installed. Either way it stays in sync as providers ship models. """ @@ -25,7 +25,7 @@ def _provider_of(name: str) -> str: - """Extract a provider key from a LiteLLM model string.""" + """Extract a provider key from a RouteHub model string.""" if "/" in name: return name.split("/", 1)[0] # Heuristics for the common no-prefix providers diff --git a/swarms/cli/tips.py b/swarms/cli/tips.py index 364c06dddc..2b9877c727 100644 --- a/swarms/cli/tips.py +++ b/swarms/cli/tips.py @@ -113,7 +113,7 @@ ] MODEL_FLAG_TIPS: List[str] = [ - "Any LiteLLM-supported model works in [bold]--model-name[/bold] — OpenAI, Anthropic, Groq, Gemini, Mistral, Cohere…", + "Any RouteHub-supported model works in [bold]--model-name[/bold] — OpenAI, Anthropic, Groq, Gemini, Mistral, Cohere…", "[bold]--model-name claude-opus-4-7[/bold] picks the latest frontier Anthropic model", "[bold]--model-name groq/llama-3.3-70b-versatile[/bold] is the fastest open-weight option", "[bold]--model-name gemini/gemini-2.5-pro[/bold] gives you million-token context natively", diff --git a/swarms/cli/utils.py b/swarms/cli/utils.py index 96ffe0d9a2..0cb3b63fe9 100644 --- a/swarms/cli/utils.py +++ b/swarms/cli/utils.py @@ -307,7 +307,7 @@ def check_dependencies() -> Tuple[bool, str, str]: Returns: Tuple containing (success, status_icon, message) """ - required_deps = ["torch", "transformers", "litellm", "rich"] + required_deps = ["torch", "transformers", "routehub", "rich"] missing_deps = [] for dep in required_deps: diff --git a/swarms/prompts/auto_agent_builder_prompt.py b/swarms/prompts/auto_agent_builder_prompt.py index a6129a3cb1..86fb56bfab 100644 --- a/swarms/prompts/auto_agent_builder_prompt.py +++ b/swarms/prompts/auto_agent_builder_prompt.py @@ -97,7 +97,7 @@ - **Structured extraction, classification, reformatting, routine summarization** — a smaller, cheaper model: `gpt-5.4-mini`, `claude-haiku-4-5-20251001`. - **High-throughput or latency-sensitive work** — a fast hosted model: `groq/llama-3.3-70b-versatile`. -Use valid LiteLLM model strings. Provider-prefixed names route correctly (`groq/`, `together_ai/`, `openrouter/`, `gemini/`); bare OpenAI names do not need a prefix. +Use valid RouteHub model strings. Provider-prefixed names route correctly (`groq/`, `together_ai/`, `openrouter/`, `gemini/`); bare OpenAI names do not need a prefix. Deliberately mixing tiers is a sign of good design. A roster where every agent runs the most expensive model usually means the work was never analyzed. So does one where every agent runs the cheapest. @@ -129,7 +129,7 @@ - The roster covers the task end to end with no gap. - Every name is unique and role-descriptive. - Every `system_prompt` is task-specific, several paragraphs, and would stand alone. -- Every `model_name` is a valid LiteLLM string, chosen for that agent's actual workload. +- Every `model_name` is a valid RouteHub string, chosen for that agent's actual workload. - The roster is within the requested maximum. Then call `build_agents` once with the full roster. diff --git a/swarms/utils/litellm_wrapper.py b/swarms/utils/litellm_wrapper.py index 6a698cfa97..06e8c159c6 100644 --- a/swarms/utils/litellm_wrapper.py +++ b/swarms/utils/litellm_wrapper.py @@ -404,12 +404,7 @@ def __init__( "num_retries": retries, "drop_params": drop_params, } - if llm_backend.BACKEND == "litellm": - litellm = llm_backend.backend_module() - for name, value in backend_options.items(): - setattr(litellm, name, value) - else: - self._backend_options = backend_options + self._backend_options = backend_options # Add system prompt if present (Anthropic rejects empty system blocks) if isinstance(self.system_prompt, str): @@ -1213,7 +1208,6 @@ def check_if_model_supports_vision(self, img: str = None): # RouteHub's catalog comes from the network, so a model it can't find may still take images. if ( out is False - and llm_backend.BACKEND == "routehub" and self.model_name not in llm_backend.model_list ): logger.warning( diff --git a/swarms/utils/llm_backend.py b/swarms/utils/llm_backend.py index 0a79e9e23b..3af2db1b5f 100644 --- a/swarms/utils/llm_backend.py +++ b/swarms/utils/llm_backend.py @@ -1,75 +1,46 @@ """The library swarms makes model calls through. -RouteHub is used when it is installed, which pip install "swarms[fast]" -does, and litellm otherwise. Set SWARMS_LLM_BACKEND to "litellm" or -"routehub" to choose one explicitly. - -Neither library is imported until a name is first used, so importing swarms +RouteHub is the only model-call backend and a required dependency. The +library is not imported until a name is first used, so importing swarms stays fast. completion, acompletion and embedding are defined here; every -other name, such as get_model_info or AuthenticationError, is read from the -chosen library on first access. +other name, such as get_model_info or AuthenticationError, is read from +RouteHub on first access. + +SWARMS_LLM_BACKEND is no longer read. A leftover "litellm" value draws a +warning so old setups learn the switch is gone; every other value is +ignored. """ import importlib -import importlib.util import os from types import ModuleType from typing import Any -BACKENDS = ("routehub", "litellm") +from loguru import logger -# The libraries refresh these themselves, so they are read on every access. +# The library refreshes these itself, so they are read on every access. _UNCACHED = frozenset({"model_list", "model_cost"}) -_INSTALL_HINTS = { - "routehub": 'pip install "swarms[fast]"', - "litellm": "pip install litellm", -} - - -def select_backend() -> str: - """Name the library to make model calls through. - - Returns: - str: "routehub" or "litellm". - - Raises: - ValueError: If SWARMS_LLM_BACKEND names another library. - ImportError: If SWARMS_LLM_BACKEND names a library that is not - installed. - """ - requested = os.getenv("SWARMS_LLM_BACKEND", "").strip().lower() - if not requested: - if importlib.util.find_spec("routehub") is not None: - return "routehub" - return "litellm" - if requested not in BACKENDS: - raise ValueError( - f"SWARMS_LLM_BACKEND must be one of {', '.join(BACKENDS)}, " - f"got {requested!r}." - ) - if importlib.util.find_spec(requested) is None: - raise ImportError( - f"SWARMS_LLM_BACKEND is {requested!r}, but {requested} is not " - f"installed. Install it with: {_INSTALL_HINTS[requested]}" - ) - return requested - - -BACKEND = select_backend() - _module = None def backend_module() -> ModuleType: - """Import the chosen library on first use. + """Import RouteHub on first use. Returns: - ModuleType: The routehub or litellm module. + ModuleType: The routehub module. """ global _module if _module is None: - _module = importlib.import_module(BACKEND) + if ( + os.getenv("SWARMS_LLM_BACKEND", "").strip().lower() + == "litellm" + ): + logger.warning( + "SWARMS_LLM_BACKEND=litellm is no longer supported; " + "swarms makes model calls through RouteHub." + ) + _module = importlib.import_module("routehub") return _module @@ -77,8 +48,8 @@ def completion(*args: Any, **kwargs: Any) -> Any: """Make a chat completion call. Args: - *args (Any): Positional arguments for the library's completion. - **kwargs (Any): Keyword arguments for the library's completion. + *args (Any): Positional arguments for RouteHub's completion. + **kwargs (Any): Keyword arguments for RouteHub's completion. Returns: Any: The response, or a stream of chunks when stream is on. @@ -90,8 +61,8 @@ async def acompletion(*args: Any, **kwargs: Any) -> Any: """Make an async chat completion call. Args: - *args (Any): Positional arguments for the library's acompletion. - **kwargs (Any): Keyword arguments for the library's acompletion. + *args (Any): Positional arguments for RouteHub's acompletion. + **kwargs (Any): Keyword arguments for RouteHub's acompletion. Returns: Any: The response, or an async stream of chunks when stream is on. @@ -103,8 +74,8 @@ def embedding(*args: Any, **kwargs: Any) -> Any: """Make an embedding call. Args: - *args (Any): Positional arguments for the library's embedding. - **kwargs (Any): Keyword arguments for the library's embedding. + *args (Any): Positional arguments for RouteHub's embedding. + **kwargs (Any): Keyword arguments for RouteHub's embedding. Returns: Any: The embedding response. @@ -113,16 +84,16 @@ def embedding(*args: Any, **kwargs: Any) -> Any: def __getattr__(name: str) -> Any: - """Read a name from the chosen library on first access. + """Read a name from RouteHub on first access. Args: - name (str): A name the library exports, such as get_model_info. + name (str): A name RouteHub exports, such as get_model_info. Returns: Any: The library's function, class or value. Raises: - AttributeError: If the library has no such name. + AttributeError: If RouteHub has no such name. """ # Dunder lookups come from tooling such as import machinery, not callers. if name.startswith("__"): diff --git a/tests/utils/test_llm_backend.py b/tests/utils/test_llm_backend.py index c408df0d93..4ad4974176 100644 --- a/tests/utils/test_llm_backend.py +++ b/tests/utils/test_llm_backend.py @@ -1,12 +1,9 @@ -"""Choosing between RouteHub and litellm, and what changes with each. +"""RouteHub is the only model-call backend. -Offline: model calls are stubbed, an installed or missing library is -simulated through importlib.util.find_spec, and backend names are replaced -in the module's namespace so nothing is fetched. +Offline: model calls are stubbed and RouteHub's names are replaced in the +module's namespace so nothing is fetched. """ -import importlib -import importlib.util import subprocess import sys from types import SimpleNamespace @@ -26,27 +23,8 @@ ) -def _with_installed(monkeypatch, *names): - """Make find_spec report only the given backends as installed.""" - real = importlib.util.find_spec - - def find_spec(name, *args): - if name in llm_backend.BACKENDS: - return object() if name in names else None - return real(name, *args) - - monkeypatch.setattr(importlib.util, "find_spec", find_spec) - - -def _use_backend(monkeypatch, backend, **names): - """Select a backend and replace names it would otherwise provide.""" - monkeypatch.setattr(llm_backend, "BACKEND", backend) - if backend == "litellm": - # LiteLLM() writes litellm's module settings, so they are restored afterwards. - litellm = importlib.import_module("litellm") - for name in BACKEND_OPTIONS: - monkeypatch.setattr(litellm, name, getattr(litellm, name)) - names.setdefault("_module", litellm) +def _use_backend(monkeypatch, **names): + """Replace names RouteHub would otherwise provide.""" namespace = vars(llm_backend) for name, value in names.items(): monkeypatch.setitem(namespace, name, value) @@ -59,40 +37,35 @@ def _response(): ) -class TestSelectBackend: - def test_routehub_is_chosen_when_installed(self, monkeypatch): - monkeypatch.delenv("SWARMS_LLM_BACKEND", raising=False) - _with_installed(monkeypatch, "routehub", "litellm") - assert llm_backend.select_backend() == "routehub" - - def test_litellm_is_chosen_without_routehub(self, monkeypatch): - monkeypatch.delenv("SWARMS_LLM_BACKEND", raising=False) - _with_installed(monkeypatch, "litellm") - assert llm_backend.select_backend() == "litellm" - - def test_env_var_forces_litellm(self, monkeypatch): - monkeypatch.setenv("SWARMS_LLM_BACKEND", " LiteLLM ") - _with_installed(monkeypatch, "routehub", "litellm") - assert llm_backend.select_backend() == "litellm" +def test_a_leftover_litellm_choice_warns_once(): + code = ( + "import os\n" + "os.environ['SWARMS_LLM_BACKEND'] = 'litellm'\n" + "import swarms.utils.llm_backend as b\n" + "b.backend_module()\n" + ) + out = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + check=True, + ) + assert "no longer supported" in out.stdout - def test_unknown_backend_is_rejected(self, monkeypatch): - monkeypatch.setenv("SWARMS_LLM_BACKEND", "openai") - with pytest.raises(ValueError, match="routehub, litellm"): - llm_backend.select_backend() - def test_missing_backend_names_the_install_command( - self, monkeypatch - ): - monkeypatch.setenv("SWARMS_LLM_BACKEND", "routehub") - _with_installed(monkeypatch, "litellm") - with pytest.raises(ImportError, match=r"swarms\[fast\]"): - llm_backend.select_backend() +def test_every_other_backend_value_is_ignored(monkeypatch, caplog): + monkeypatch.setenv("SWARMS_LLM_BACKEND", "openai") + module = SimpleNamespace(completion=lambda: "value") + monkeypatch.setattr(llm_backend, "_module", module) + with caplog.at_level("WARNING"): + assert llm_backend.backend_module() is module + assert "SWARMS_LLM_BACKEND" not in caplog.text -def test_import_swarms_loads_neither_library(): +def test_import_swarms_loads_no_backend_library(): code = ( "import sys, swarms; from swarms import Agent; " - "print('litellm' in sys.modules, 'routehub' in sys.modules)" + "print('routehub' in sys.modules)" ) out = subprocess.run( [sys.executable, "-c", code], @@ -100,7 +73,7 @@ def test_import_swarms_loads_neither_library(): text=True, check=True, ) - assert out.stdout.split()[-2:] == ["False", "False"] + assert out.stdout.split()[-1] == "False" class TestOptionsPerCall: @@ -118,7 +91,7 @@ def _sent(self): def test_routehub_gets_the_options_with_each_call( self, monkeypatch ): - _use_backend(monkeypatch, "routehub") + _use_backend(monkeypatch) sent = self._sent() assert {name: sent[name] for name in BACKEND_OPTIONS} == { "set_verbose": False, @@ -127,20 +100,11 @@ def test_routehub_gets_the_options_with_each_call( "drop_params": True, } - def test_litellm_keeps_them_as_module_settings(self, monkeypatch): - _use_backend(monkeypatch, "litellm") - - sent = self._sent() - - assert not set(BACKEND_OPTIONS) & set(sent) - assert llm_backend.backend_module().num_retries == 5 - class TestVisionCheck: - def _llm(self, monkeypatch, backend, known_models, model): + def _llm(self, monkeypatch, known_models, model): _use_backend( monkeypatch, - backend, supports_vision=lambda model: False, model_list=known_models, ) @@ -149,7 +113,7 @@ def _llm(self, monkeypatch, backend, known_models, model): def test_routehub_sends_images_to_a_model_it_cannot_find( self, monkeypatch ): - llm = self._llm(monkeypatch, "routehub", [], "lab/unlisted") + llm = self._llm(monkeypatch, [], "lab/unlisted") llm.check_if_model_supports_vision(img="photo.png") def test_a_listed_text_only_model_is_still_refused( @@ -157,26 +121,18 @@ def test_a_listed_text_only_model_is_still_refused( ): llm = self._llm( monkeypatch, - "routehub", ["lab/text-only"], "lab/text-only", ) with pytest.raises(ValueError, match="does not support"): llm.check_if_model_supports_vision(img="photo.png") - def test_litellm_refuses_an_unlisted_model_as_before( - self, monkeypatch - ): - llm = self._llm(monkeypatch, "litellm", [], "lab/unlisted-2") - with pytest.raises(ValueError, match="does not support"): - llm.check_if_model_supports_vision(img="photo.png") - def test_a_no_is_asked_again(self, monkeypatch): answers = iter([False, True]) _use_backend( monkeypatch, - "litellm", supports_vision=lambda model: next(answers), + model_list=["lab/catalog-was-down"], ) llm = LiteLLM(model_name="lab/catalog-was-down") @@ -189,7 +145,6 @@ class TestUnknownModelWarning: def _warnings(self, monkeypatch, known_models): _use_backend( monkeypatch, - "routehub", model_list=known_models, get_max_tokens=lambda model: 1000, )