diff --git a/litellm/llms/openai_like/chat/transformation.py b/litellm/llms/openai_like/chat/transformation.py index 030710c8b2d..0ca1602ecef 100644 --- a/litellm/llms/openai_like/chat/transformation.py +++ b/litellm/llms/openai_like/chat/transformation.py @@ -168,3 +168,11 @@ class OpenAILikeChatConfig(OpenAIGPTConfig): mapped_params.pop("max_completion_tokens", None) return mapped_params + + +class CustomOpenAIChatConfig(OpenAILikeChatConfig): + def get_models(self, api_key: str | None = None, api_base: str | None = None) -> list[str]: + if api_base is None: + raise ValueError("api_base must be set to discover models for the custom_openai provider") + models = super().get_models(api_key=api_key or "", api_base=api_base) + return [f"custom_openai/{model}" for model in models] diff --git a/litellm/utils.py b/litellm/utils.py index d0e11bc9551..b37d9779c10 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8770,6 +8770,16 @@ class ProviderConfigManager: return litellm.InceptionTextCompletionConfig() return litellm.OpenAITextCompletionConfig() + @staticmethod + def _openai_family_model_info(provider: LlmProviders) -> BaseLLMModelInfo: + if provider == LlmProviders.CUSTOM_OPENAI: + from litellm.llms.openai_like.chat.transformation import ( + CustomOpenAIChatConfig, + ) + + return CustomOpenAIChatConfig() + return litellm.OpenAIGPTConfig() + @staticmethod def get_provider_model_info( model: str | None, @@ -8777,8 +8787,8 @@ class ProviderConfigManager: ) -> BaseLLMModelInfo | None: if LlmProviders.FIREWORKS_AI == provider: return litellm.FireworksAIConfig() - elif LlmProviders.OPENAI == provider: - return litellm.OpenAIGPTConfig() + elif provider in (LlmProviders.OPENAI, LlmProviders.CUSTOM_OPENAI): + return ProviderConfigManager._openai_family_model_info(provider) elif LlmProviders.GEMINI == provider: return litellm.GeminiModelInfo() elif LlmProviders.VERTEX_AI == provider: diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index 0ccfae55290..8692ec7edd1 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1638,6 +1638,7 @@ def test_get_valid_models_openai_proxy(monkeypatch): assert "litellm_proxy/gpt-5.5" in valid_models + def test_get_valid_models_fireworks_ai(monkeypatch): from litellm.utils import get_valid_models import litellm diff --git a/tests/test_litellm/llms/openai_like/chat/test_openai_like_chat_transformation.py b/tests/test_litellm/llms/openai_like/chat/test_openai_like_chat_transformation.py index 88382b80d16..e7c71ec91be 100644 --- a/tests/test_litellm/llms/openai_like/chat/test_openai_like_chat_transformation.py +++ b/tests/test_litellm/llms/openai_like/chat/test_openai_like_chat_transformation.py @@ -1,5 +1,14 @@ +from unittest.mock import MagicMock, patch + +import litellm import pytest -from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig +from litellm.llms.openai_like.chat.transformation import ( + CustomOpenAIChatConfig, + OpenAILikeChatConfig, +) +from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig +from litellm.types.utils import LlmProviders +from litellm.utils import ProviderConfigManager, get_valid_models def test_sanitize_usage_obj_handles_null_tokens(): @@ -47,3 +56,68 @@ def test_sanitize_usage_obj_valid_usage(): # Assert assert sanitized_json == original_json # The object should be unchanged + + +def test_get_valid_models_custom_openai(): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "object": "list", + "data": [ + { + "id": "my-custom-model", + "object": "model", + "created": 1686935002, + "owned_by": "organization-owner", + }, + ], + } + + with patch.object(litellm.module_level_client, "get", return_value=mock_response) as mock_get: + valid_models = get_valid_models( + check_provider_endpoint=True, + custom_llm_provider="custom_openai", + api_key="sk-1234", + api_base="https://my-openai-compatible-endpoint/v1", + ) + + assert valid_models == ["custom_openai/my-custom-model"] + mock_get.assert_called_once_with( + url="https://my-openai-compatible-endpoint/v1/models", + headers={"Authorization": "Bearer sk-1234"}, + ) + + +def test_custom_openai_get_models_requires_api_base(): + with pytest.raises( + ValueError, + match="api_base must be set to discover models for the custom_openai provider", + ): + CustomOpenAIChatConfig().get_models(api_base=None) + + +def test_custom_openai_get_models_does_not_use_openai_api_key(monkeypatch): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"data": [{"id": "my-custom-model"}]} + monkeypatch.setenv("OPENAI_API_KEY", "unrelated-openai-api-key") + + with patch.object(litellm.module_level_client, "get", return_value=mock_response) as mock_get: + models = CustomOpenAIChatConfig().get_models( + api_base="https://my-openai-compatible-endpoint/v1" + ) + + assert models == ["custom_openai/my-custom-model"] + mock_get.assert_called_once_with( + url="https://my-openai-compatible-endpoint/v1/models", + headers={"Authorization": "Bearer "}, + ) + + +def test_openai_model_info_uses_openai_config(): + config = ProviderConfigManager.get_provider_model_info( + model=None, + provider=LlmProviders.OPENAI, + ) + + assert type(config) is OpenAIGPTConfig