Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/sdk/server-ai/src/ldai/providers/runner_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
# Multi-provider packages should be last in the list.
SUPPORTED_AI_PROVIDERS = ('openai', 'langchain')

# Mapping from internal Python module name to the pip-installable PyPI package name.
_PYPI_PACKAGE_NAMES = {
'ldai_openai': 'launchdarkly-server-sdk-ai-openai',
'ldai_langchain': 'launchdarkly-server-sdk-ai-langchain',
}


class RunnerFactory:
"""
Expand Down Expand Up @@ -51,10 +57,7 @@ def _get_provider_factory(provider_type: str) -> Optional[AIProvider]:
)
return None
except ImportError as error:
log.warning(
f"Could not load provider '{provider_type}': {error}. "
f"Make sure the corresponding package is installed."
)
log.warning(f"Could not load provider '{provider_type}': {error}")
return None

@staticmethod
Expand Down Expand Up @@ -188,4 +191,5 @@ def _pkg_exists(package_name: str) -> None:
:param package_name: Name of the package to check
"""
if util.find_spec(package_name) is None:
raise ImportError(f"Package '{package_name}' not found")
pypi_name = _PYPI_PACKAGE_NAMES.get(package_name, package_name)
raise ImportError(f"Package '{pypi_name}' not found. Make sure it is installed.")
84 changes: 84 additions & 0 deletions packages/sdk/server-ai/tests/test_runner_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for RunnerFactory provider loading and error messages."""

from unittest.mock import patch

import pytest

from ldai.providers.runner_factory import RunnerFactory, _PYPI_PACKAGE_NAMES


class TestPkgExists:
"""_pkg_exists raises ImportError with the PyPI package name when a module is missing."""

def test_raises_import_error_with_pypi_name_for_openai(self):
with patch('ldai.providers.runner_factory.util') as mock_util:
mock_util.find_spec.return_value = None
with pytest.raises(ImportError) as exc_info:
RunnerFactory._pkg_exists('ldai_openai')
assert 'launchdarkly-server-sdk-ai-openai' in str(exc_info.value)
assert 'pip install' not in str(exc_info.value)

def test_raises_import_error_with_pypi_name_for_langchain(self):
with patch('ldai.providers.runner_factory.util') as mock_util:
mock_util.find_spec.return_value = None
with pytest.raises(ImportError) as exc_info:
RunnerFactory._pkg_exists('ldai_langchain')
assert 'launchdarkly-server-sdk-ai-langchain' in str(exc_info.value)
assert 'pip install' not in str(exc_info.value)

def test_raises_import_error_with_module_name_when_no_mapping(self):
"""Unknown module names fall back to the module name itself."""
with patch('ldai.providers.runner_factory.util') as mock_util:
mock_util.find_spec.return_value = None
with pytest.raises(ImportError) as exc_info:
RunnerFactory._pkg_exists('some_unknown_module')
assert 'some_unknown_module' in str(exc_info.value)

def test_does_not_raise_when_package_is_found(self):
with patch('ldai.providers.runner_factory.util') as mock_util:
mock_util.find_spec.return_value = object() # non-None means found
# Should not raise
RunnerFactory._pkg_exists('ldai_openai')


class TestPypiPackageNameMapping:
"""The _PYPI_PACKAGE_NAMES mapping covers all supported providers."""

def test_openai_module_maps_to_pypi_name(self):
assert _PYPI_PACKAGE_NAMES['ldai_openai'] == 'launchdarkly-server-sdk-ai-openai'

def test_langchain_module_maps_to_pypi_name(self):
assert _PYPI_PACKAGE_NAMES['ldai_langchain'] == 'launchdarkly-server-sdk-ai-langchain'


class TestGetProviderFactory:
"""_get_provider_factory logs the PyPI package name in its warning when a package is missing."""

def test_warning_includes_pypi_name_for_openai(self):
with patch('ldai.providers.runner_factory.util') as mock_util, \
patch('ldai.providers.runner_factory.log') as mock_log:
mock_util.find_spec.return_value = None
result = RunnerFactory._get_provider_factory('openai')
assert result is None
warning_text = mock_log.warning.call_args[0][0]
assert 'launchdarkly-server-sdk-ai-openai' in warning_text
assert 'ldai_openai' not in warning_text

def test_warning_includes_pypi_name_for_langchain(self):
with patch('ldai.providers.runner_factory.util') as mock_util, \
patch('ldai.providers.runner_factory.log') as mock_log:
mock_util.find_spec.return_value = None
result = RunnerFactory._get_provider_factory('langchain')
assert result is None
warning_text = mock_log.warning.call_args[0][0]
assert 'launchdarkly-server-sdk-ai-langchain' in warning_text
assert 'ldai_langchain' not in warning_text

def test_warning_does_not_reference_pip(self):
"""Warning should be package-manager agnostic — no pip install command."""
with patch('ldai.providers.runner_factory.util') as mock_util, \
patch('ldai.providers.runner_factory.log') as mock_log:
mock_util.find_spec.return_value = None
RunnerFactory._get_provider_factory('openai')
warning_text = mock_log.warning.call_args[0][0]
assert 'pip install' not in warning_text
Loading