From b382650ec544ee37aa959dd63f26128950b260fd Mon Sep 17 00:00:00 2001 From: Emmanuel Acheampong Date: Thu, 12 Mar 2026 14:15:44 -0700 Subject: [PATCH] refactor(crusoe): simplify to JSON-based provider registration Replace hand-written CrusoeChatConfig class and manual registrations across constants.py, __init__.py, get_llm_provider_logic.py, and _lazy_imports_registry.py with a single entry in litellm/llms/openai_like/providers.json, consistent with the recommended pattern for OpenAI-compatible providers. Co-Authored-By: Claude Sonnet 4.6 --- litellm/__init__.py | 4 - litellm/_lazy_imports_registry.py | 1 - litellm/constants.py | 4 - .../get_llm_provider_logic.py | 10 -- litellm/llms/crusoe/__init__.py | 0 litellm/llms/crusoe/chat/__init__.py | 0 litellm/llms/crusoe/chat/transformation.py | 42 ------- litellm/llms/openai_like/providers.json | 5 + tests/llm_translation/test_crusoe.py | 93 +++------------ tests/test_litellm/llms/crusoe/test_crusoe.py | 111 ++++++++++++++++++ 10 files changed, 130 insertions(+), 140 deletions(-) delete mode 100644 litellm/llms/crusoe/__init__.py delete mode 100644 litellm/llms/crusoe/chat/__init__.py delete mode 100644 litellm/llms/crusoe/chat/transformation.py create mode 100644 tests/test_litellm/llms/crusoe/test_crusoe.py diff --git a/litellm/__init__.py b/litellm/__init__.py index 4931b8cc77a..7f72e0b0e89 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -577,7 +577,6 @@ publicai_models: Set = set() v0_models: Set = set() morph_models: Set = set() lambda_ai_models: Set = set() -crusoe_models: Set = set() hyperbolic_models: Set = set() black_forest_labs_models: Set = set() recraft_models: Set = set() @@ -825,8 +824,6 @@ def add_known_models(model_cost_map: Optional[Dict] = None): morph_models.add(key) elif value.get("litellm_provider") == "lambda_ai": lambda_ai_models.add(key) - elif value.get("litellm_provider") == "crusoe": - crusoe_models.add(key) elif value.get("litellm_provider") == "hyperbolic": hyperbolic_models.add(key) elif value.get("litellm_provider") == "black_forest_labs": @@ -1062,7 +1059,6 @@ models_by_provider: dict = { "v0": v0_models, "morph": morph_models, "lambda_ai": lambda_ai_models, - "crusoe": crusoe_models, "hyperbolic": hyperbolic_models, "black_forest_labs": black_forest_labs_models, "recraft": recraft_models, diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index 93dd32df495..9164a3c8ae4 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -1135,7 +1135,6 @@ _LLM_CONFIGS_IMPORT_MAP = { "MorphChatConfig": (".llms.morph.chat.transformation", "MorphChatConfig"), "RAGFlowConfig": (".llms.ragflow.chat.transformation", "RAGFlowConfig"), "LambdaAIChatConfig": (".llms.lambda_ai.chat.transformation", "LambdaAIChatConfig"), - "CrusoeChatConfig": (".llms.crusoe.chat.transformation", "CrusoeChatConfig"), "HyperbolicChatConfig": ( ".llms.hyperbolic.chat.transformation", "HyperbolicChatConfig", diff --git a/litellm/constants.py b/litellm/constants.py index 6663ee59395..89c59ee9326 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -560,7 +560,6 @@ LITELLM_CHAT_PROVIDERS = [ "oci", "morph", "lambda_ai", - "crusoe", "vercel_ai_gateway", "wandb", "ovhcloud", @@ -719,7 +718,6 @@ openai_compatible_endpoints: List = [ "https://api.v0.dev/v1", "https://api.morphllm.com/v1", "https://api.lambda.ai/v1", - "https://managed-inference-api-proxy.crusoecloud.com/v1/", "https://api.hyperbolic.xyz/v1", "https://ai-gateway.helicone.ai/", "https://ai-gateway.vercel.sh/v1", @@ -775,7 +773,6 @@ openai_compatible_providers: List = [ "helicone", "morph", "lambda_ai", - "crusoe", "hyperbolic", "vercel_ai_gateway", "aiml", @@ -804,7 +801,6 @@ openai_text_completion_compatible_providers: List = ( "chutes", "v0", "lambda_ai", - "crusoe", "hyperbolic", "wandb", ] diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index ad5297ef53e..36218417377 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -315,9 +315,6 @@ def get_llm_provider( # noqa: PLR0915 elif endpoint == "https://api.lambda.ai/v1": custom_llm_provider = "lambda_ai" dynamic_api_key = get_secret_str("LAMBDA_API_KEY") - elif endpoint == "https://managed-inference-api-proxy.crusoecloud.com/v1/": - custom_llm_provider = "crusoe" - dynamic_api_key = get_secret_str("CRUSOE_API_KEY") elif endpoint == "https://api.hyperbolic.xyz/v1": custom_llm_provider = "hyperbolic" dynamic_api_key = get_secret_str("HYPERBOLIC_API_KEY") @@ -883,13 +880,6 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 ) = litellm.LambdaAIChatConfig()._get_openai_compatible_provider_info( api_base, api_key ) - elif custom_llm_provider == "crusoe": - ( - api_base, - dynamic_api_key, - ) = litellm.CrusoeChatConfig()._get_openai_compatible_provider_info( - api_base, api_key - ) elif custom_llm_provider == "hyperbolic": ( api_base, diff --git a/litellm/llms/crusoe/__init__.py b/litellm/llms/crusoe/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/litellm/llms/crusoe/chat/__init__.py b/litellm/llms/crusoe/chat/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/litellm/llms/crusoe/chat/transformation.py b/litellm/llms/crusoe/chat/transformation.py deleted file mode 100644 index dab9a9f833b..00000000000 --- a/litellm/llms/crusoe/chat/transformation.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Translate from OpenAI's `/v1/chat/completions` to Crusoe's `/v1/chat/completions` -""" - -from typing import Optional, Tuple - -from litellm.secret_managers.main import get_secret_str - -from ...openai_like.chat.transformation import OpenAILikeChatConfig - - -class CrusoeChatConfig(OpenAILikeChatConfig): - """ - Crusoe is OpenAI-compatible with standard endpoints. - - Docs: https://docs.crusoecloud.com/managed-inference/overview/index.html - """ - - @property - def custom_llm_provider(self) -> Optional[str]: - return "crusoe" - - def _get_openai_compatible_provider_info( - self, api_base: Optional[str], api_key: Optional[str] - ) -> Tuple[Optional[str], Optional[str]]: - api_base = ( - api_base - or get_secret_str("CRUSOE_API_BASE") - or "https://managed-inference-api-proxy.crusoecloud.com/v1/" - ) # type: ignore - dynamic_api_key = api_key or get_secret_str("CRUSOE_API_KEY") - return api_base, dynamic_api_key - - def get_supported_openai_params(self, model: str) -> list: - return [ - "messages", - "model", - "temperature", - "top_p", - "frequency_penalty", - "presence_penalty", - ] diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 275c352b39e..9021f9fc52c 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -101,5 +101,10 @@ "param_mappings": { "max_completion_tokens": "max_tokens" } + }, + "crusoe": { + "base_url": "https://managed-inference-api-proxy.crusoecloud.com/v1/", + "api_key_env": "CRUSOE_API_KEY", + "api_base_env": "CRUSOE_API_BASE" } } diff --git a/tests/llm_translation/test_crusoe.py b/tests/llm_translation/test_crusoe.py index 254d6530a42..064f4f053bc 100644 --- a/tests/llm_translation/test_crusoe.py +++ b/tests/llm_translation/test_crusoe.py @@ -4,24 +4,29 @@ Tests for Crusoe provider integration import os from unittest import mock -import pytest - import litellm -from litellm import completion -from litellm.llms.crusoe.chat.transformation import CrusoeChatConfig CRUSOE_API_BASE = "https://managed-inference-api-proxy.crusoecloud.com/v1/" -def test_crusoe_config_initialization(): - """Test CrusoeChatConfig initializes correctly""" - config = CrusoeChatConfig() - assert config.custom_llm_provider == "crusoe" +def test_crusoe_json_registry(): + """Test CrusoeChatConfig is loaded from JSON provider registry""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + assert JSONProviderRegistry.exists("crusoe") + config = JSONProviderRegistry.get("crusoe") + assert config is not None + assert config.base_url == CRUSOE_API_BASE + assert config.api_key_env == "CRUSOE_API_KEY" + assert config.api_base_env == "CRUSOE_API_BASE" def test_crusoe_get_openai_compatible_provider_info(): """Test Crusoe provider info retrieval""" - config = CrusoeChatConfig() + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + config = create_config_class(JSONProviderRegistry.get("crusoe"))() # Test with default values (no env vars set) with mock.patch.dict(os.environ, {}, clear=True): @@ -67,22 +72,6 @@ def test_get_llm_provider_crusoe(): assert model == "meta-llama/Llama-3.3-70B-Instruct" assert provider == "crusoe" - # Test with api_base containing Crusoe endpoint - model, provider, api_key, api_base = get_llm_provider( - "meta-llama/Llama-3.3-70B-Instruct", - api_base=CRUSOE_API_BASE, - ) - assert model == "meta-llama/Llama-3.3-70B-Instruct" - assert provider == "crusoe" - assert api_base == CRUSOE_API_BASE - - -def test_crusoe_in_provider_lists(): - """Test that Crusoe is registered in all necessary provider lists""" - assert "crusoe" in litellm.openai_compatible_providers - assert "crusoe" in litellm.provider_list - assert CRUSOE_API_BASE in litellm.openai_compatible_endpoints - def test_crusoe_models_configuration(): """Test that Crusoe models are configured correctly""" @@ -91,9 +80,6 @@ def test_crusoe_models_configuration(): os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" litellm.model_cost = litellm.get_model_cost_map(url="") - litellm.crusoe_models = set() - litellm.add_known_models() - crusoe_models = [ "crusoe/meta-llama/Llama-3.3-70B-Instruct", "crusoe/deepseek-ai/DeepSeek-R1-0528", @@ -111,54 +97,3 @@ def test_crusoe_models_configuration(): f"{model} should have crusoe as provider" ) assert model_info.get("mode") == "chat", f"{model} should be in chat mode" - - -def test_crusoe_model_list_populated(): - """Test that crusoe_models list is populated correctly""" - os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" - litellm.model_cost = litellm.get_model_cost_map(url="") - - litellm.crusoe_models = set() - litellm.add_known_models() - - assert len(litellm.crusoe_models) > 0, "crusoe_models list should not be empty" - - for model in litellm.crusoe_models: - assert model.startswith("crusoe/"), ( - f"Model {model} should start with 'crusoe/'" - ) - - expected_models = [ - "crusoe/meta-llama/Llama-3.3-70B-Instruct", - "crusoe/deepseek-ai/DeepSeek-R1-0528", - "crusoe/deepseek-ai/DeepSeek-V3-0324", - "crusoe/Qwen/Qwen3-235B-A22B-Instruct-2507", - "crusoe/moonshotai/Kimi-K2-Thinking", - "crusoe/openai/gpt-oss-120b", - "crusoe/google/gemma-3-12b-it", - ] - - for model in expected_models: - assert model in litellm.crusoe_models, ( - f"{model} should be in crusoe_models list" - ) - - -@pytest.mark.asyncio -async def test_crusoe_completion_call(): - """Test completion call with Crusoe provider (requires CRUSOE_API_KEY)""" - if not os.getenv("CRUSOE_API_KEY"): - pytest.skip("CRUSOE_API_KEY not set") - - try: - response = await litellm.acompletion( - model="crusoe/meta-llama/Llama-3.3-70B-Instruct", - messages=[{"role": "user", "content": "Hello, this is a test"}], - max_tokens=10, - ) - assert response.choices[0].message.content - assert response.model - assert response.usage - except Exception as e: - if "crusoe" not in str(e) and "provider" not in str(e).lower(): - raise diff --git a/tests/test_litellm/llms/crusoe/test_crusoe.py b/tests/test_litellm/llms/crusoe/test_crusoe.py new file mode 100644 index 00000000000..6ca73178047 --- /dev/null +++ b/tests/test_litellm/llms/crusoe/test_crusoe.py @@ -0,0 +1,111 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../../..")) +from unittest.mock import patch + +CRUSOE_API_BASE = "https://managed-inference-api-proxy.crusoecloud.com/v1/" + + +def test_crusoe_json_registry(): + """Test Crusoe is registered in the JSON provider registry""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + assert JSONProviderRegistry.exists("crusoe") + config = JSONProviderRegistry.get("crusoe") + assert config is not None + assert config.base_url == CRUSOE_API_BASE + assert config.api_key_env == "CRUSOE_API_KEY" + assert config.api_base_env == "CRUSOE_API_BASE" + + +def test_crusoe_dynamic_config_defaults(): + """Test dynamic config returns correct default API base""" + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + config = create_config_class(JSONProviderRegistry.get("crusoe"))() + + with patch.dict(os.environ, {}, clear=True): + api_base, api_key = config._get_openai_compatible_provider_info(None, None) + + assert api_base == CRUSOE_API_BASE + assert api_key is None + + +def test_crusoe_dynamic_config_env_vars(): + """Test dynamic config reads CRUSOE_API_KEY and CRUSOE_API_BASE from env""" + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + config = create_config_class(JSONProviderRegistry.get("crusoe"))() + + with patch.dict( + os.environ, + {"CRUSOE_API_KEY": "test-key", "CRUSOE_API_BASE": "https://custom.crusoe.com/v1/"}, + ): + api_base, api_key = config._get_openai_compatible_provider_info(None, None) + + assert api_base == "https://custom.crusoe.com/v1/" + assert api_key == "test-key" + + +def test_crusoe_dynamic_config_explicit_params(): + """Test explicit params override env vars""" + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + config = create_config_class(JSONProviderRegistry.get("crusoe"))() + + with patch.dict(os.environ, {"CRUSOE_API_KEY": "env-key"}): + api_base, api_key = config._get_openai_compatible_provider_info( + "https://override.crusoe.com/v1/", "override-key" + ) + + assert api_base == "https://override.crusoe.com/v1/" + assert api_key == "override-key" + + +def test_crusoe_supported_params(): + """Test dynamic config returns standard OpenAI params""" + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + config = create_config_class(JSONProviderRegistry.get("crusoe"))() + params = config.get_supported_openai_params(model="meta-llama/Llama-3.3-70B-Instruct") + + assert isinstance(params, list) + assert len(params) > 0 + assert "temperature" in params + assert "max_tokens" in params + assert "stream" in params + + +def test_crusoe_provider_detection_by_prefix(): + """Test crusoe/model prefix is correctly routed""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, _, _ = get_llm_provider("crusoe/meta-llama/Llama-3.3-70B-Instruct") + assert provider == "crusoe" + assert model == "meta-llama/Llama-3.3-70B-Instruct" + + +def test_crusoe_model_list_populated(): + """Test Crusoe models are present in model_prices_and_context_window.json""" + import litellm + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + expected = [ + "crusoe/meta-llama/Llama-3.3-70B-Instruct", + "crusoe/deepseek-ai/DeepSeek-R1-0528", + "crusoe/deepseek-ai/DeepSeek-V3-0324", + "crusoe/Qwen/Qwen3-235B-A22B-Instruct-2507", + "crusoe/moonshotai/Kimi-K2-Thinking", + "crusoe/openai/gpt-oss-120b", + "crusoe/google/gemma-3-12b-it", + ] + for model in expected: + assert model in litellm.model_cost, f"{model} not found in model_cost" + assert litellm.model_cost[model].get("litellm_provider") == "crusoe"