From 8e2e90bcab4f152862edf5f7d7c127dd298b006b Mon Sep 17 00:00:00 2001 From: Mason Daugherty <61371264+mdrxy@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:27:44 +0000 Subject: [PATCH] feat(langchain): expose provider package via get_provider_package Add an optional pypi_name field to the built-in provider registries (now typed _ProviderSpec NamedTuples) in chat_models and embeddings, plus a public get_provider_package accessor. Downstream consumers can map a provider key to its pip package without importing private symbols or re-deriving the name from module paths. Co-authored-by: open-swe[bot] --- .../langchain/chat_models/__init__.py | 4 +- .../langchain/chat_models/base.py | 148 ++++++++++++------ .../langchain/embeddings/__init__.py | 3 +- .../langchain_v1/langchain/embeddings/base.py | 106 ++++++++++--- .../chat_models/test_chat_models.py | 25 ++- .../tests/unit_tests/embeddings/test_base.py | 20 +++ 6 files changed, 233 insertions(+), 73 deletions(-) diff --git a/libs/langchain_v1/langchain/chat_models/__init__.py b/libs/langchain_v1/langchain/chat_models/__init__.py index c6900dea8b2..66d17307c4e 100644 --- a/libs/langchain_v1/langchain/chat_models/__init__.py +++ b/libs/langchain_v1/langchain/chat_models/__init__.py @@ -2,6 +2,6 @@ from langchain_core.language_models import BaseChatModel -from langchain.chat_models.base import init_chat_model +from langchain.chat_models.base import get_provider_package, init_chat_model -__all__ = ["BaseChatModel", "init_chat_model"] +__all__ = ["BaseChatModel", "get_provider_package", "init_chat_model"] diff --git a/libs/langchain_v1/langchain/chat_models/base.py b/libs/langchain_v1/langchain/chat_models/base.py index 89b7f9da1ab..2673e3d85fd 100644 --- a/libs/langchain_v1/langchain/chat_models/base.py +++ b/libs/langchain_v1/langchain/chat_models/base.py @@ -9,6 +9,7 @@ from typing import ( TYPE_CHECKING, Any, Literal, + NamedTuple, TypeAlias, cast, overload, @@ -35,56 +36,87 @@ def _call(cls: type[BaseChatModel], **kwargs: Any) -> BaseChatModel: return cls(**kwargs) -_BUILTIN_PROVIDERS: dict[str, tuple[str, str, Callable[..., BaseChatModel]]] = { - "anthropic": ("langchain_anthropic", "ChatAnthropic", _call), - "anthropic_bedrock": ("langchain_aws", "ChatAnthropicBedrock", _call), - "azure_ai": ("langchain_azure_ai.chat_models", "AzureAIOpenAIApiChatModel", _call), - "azure_openai": ("langchain_openai", "AzureChatOpenAI", _call), - "baseten": ("langchain_baseten", "ChatBaseten", _call), - "bedrock": ("langchain_aws", "ChatBedrock", _call), - "bedrock_converse": ("langchain_aws", "ChatBedrockConverse", _call), - "cohere": ("langchain_cohere", "ChatCohere", _call), - "deepseek": ("langchain_deepseek", "ChatDeepSeek", _call), - "fireworks": ("langchain_fireworks", "ChatFireworks", _call), - "google_anthropic_vertex": ( +class _ProviderSpec(NamedTuple): + """Import configuration for a built-in chat model provider. + + This is a `NamedTuple` for backwards compatibility with the previous + `(module, class_name, creator)` tuple: the first three fields keep their + positions, so existing positional indexing (e.g. `spec[0]`) and iteration + continue to work. + """ + + module: str + """Python module path containing the chat model class. + + May be a submodule (e.g. `'langchain_azure_ai.chat_models'`) when the class is + not exported from the package root. + """ + class_name: str + """Name of the chat model class to import.""" + creator: Callable[..., BaseChatModel] + """Callable that instantiates the class with provided kwargs.""" + pypi_name: str | None = None + """PyPI distribution name, set only when it differs from the derived default. + + When `None`, `package` derives the name from `module` (the first module + segment with underscores replaced by hyphens), which holds for every LangChain + integration package. Set this explicitly for providers whose import module and + PyPI distribution name diverge. + """ + + @property + def package(self) -> str: + """PyPI distribution name that provides this provider's integration.""" + if self.pypi_name is not None: + return self.pypi_name + return self.module.split(".", maxsplit=1)[0].replace("_", "-") + + +_BUILTIN_PROVIDERS: dict[str, _ProviderSpec] = { + "anthropic": _ProviderSpec("langchain_anthropic", "ChatAnthropic", _call), + "anthropic_bedrock": _ProviderSpec("langchain_aws", "ChatAnthropicBedrock", _call), + "azure_ai": _ProviderSpec("langchain_azure_ai.chat_models", "AzureAIOpenAIApiChatModel", _call), + "azure_openai": _ProviderSpec("langchain_openai", "AzureChatOpenAI", _call), + "baseten": _ProviderSpec("langchain_baseten", "ChatBaseten", _call), + "bedrock": _ProviderSpec("langchain_aws", "ChatBedrock", _call), + "bedrock_converse": _ProviderSpec("langchain_aws", "ChatBedrockConverse", _call), + "cohere": _ProviderSpec("langchain_cohere", "ChatCohere", _call), + "deepseek": _ProviderSpec("langchain_deepseek", "ChatDeepSeek", _call), + "fireworks": _ProviderSpec("langchain_fireworks", "ChatFireworks", _call), + "google_anthropic_vertex": _ProviderSpec( "langchain_google_vertexai.model_garden", "ChatAnthropicVertex", _call, ), - "google_genai": ("langchain_google_genai", "ChatGoogleGenerativeAI", _call), - "google_vertexai": ("langchain_google_vertexai", "ChatVertexAI", _call), - "groq": ("langchain_groq", "ChatGroq", _call), - "huggingface": ( + "google_genai": _ProviderSpec("langchain_google_genai", "ChatGoogleGenerativeAI", _call), + "google_vertexai": _ProviderSpec("langchain_google_vertexai", "ChatVertexAI", _call), + "groq": _ProviderSpec("langchain_groq", "ChatGroq", _call), + "huggingface": _ProviderSpec( "langchain_huggingface", "ChatHuggingFace", lambda cls, model, **kwargs: cls.from_model_id(model_id=model, **kwargs), ), - "ibm": ( + "ibm": _ProviderSpec( "langchain_ibm", "ChatWatsonx", lambda cls, model, **kwargs: cls(model_id=model, **kwargs), ), - "litellm": ("langchain_litellm", "ChatLiteLLM", _call), - "mistralai": ("langchain_mistralai", "ChatMistralAI", _call), - "nvidia": ("langchain_nvidia_ai_endpoints", "ChatNVIDIA", _call), - "ollama": ("langchain_ollama", "ChatOllama", _call), - "openai": ("langchain_openai", "ChatOpenAI", _call), - "openrouter": ("langchain_openrouter", "ChatOpenRouter", _call), - "perplexity": ("langchain_perplexity", "ChatPerplexity", _call), - "together": ("langchain_together", "ChatTogether", _call), - "upstage": ("langchain_upstage", "ChatUpstage", _call), - "xai": ("langchain_xai", "ChatXAI", _call), + "litellm": _ProviderSpec("langchain_litellm", "ChatLiteLLM", _call), + "mistralai": _ProviderSpec("langchain_mistralai", "ChatMistralAI", _call), + "nvidia": _ProviderSpec("langchain_nvidia_ai_endpoints", "ChatNVIDIA", _call), + "ollama": _ProviderSpec("langchain_ollama", "ChatOllama", _call), + "openai": _ProviderSpec("langchain_openai", "ChatOpenAI", _call), + "openrouter": _ProviderSpec("langchain_openrouter", "ChatOpenRouter", _call), + "perplexity": _ProviderSpec("langchain_perplexity", "ChatPerplexity", _call), + "together": _ProviderSpec("langchain_together", "ChatTogether", _call), + "upstage": _ProviderSpec("langchain_upstage", "ChatUpstage", _call), + "xai": _ProviderSpec("langchain_xai", "ChatXAI", _call), } """Registry mapping provider names to their import configuration. -Each entry maps a provider key to a tuple of: - -- `module_path`: The Python module path containing the chat model class. - - This may be a submodule (e.g., `'langchain_azure_ai.chat_models'`) if the class is - not exported from the package root. -- `class_name`: The name of the chat model class to import. -- `creator_func`: A callable that instantiates the class with provided kwargs. +Each entry maps a provider key to a `_ProviderSpec`, holding the import module +path, the chat model class name, a callable that instantiates it, and an optional +explicit PyPI distribution name (see `_ProviderSpec.package`). !!! note @@ -100,12 +132,41 @@ Each entry maps a provider key to a tuple of: """ -def _import_module(module: str, class_name: str) -> ModuleType: +def get_provider_package(provider: str) -> str: + """Return the PyPI package that provides a built-in provider's integration. + + Use this to map a provider key (e.g. `'openai'`) to the pip-installable + distribution name (e.g. `'langchain-openai'`) without hard-coding the mapping + or re-deriving it from module paths. + + Args: + provider: A built-in provider key, as accepted by `init_chat_model`'s + `model_provider` argument (e.g. `'openai'`, `'anthropic'`). + + Returns: + The pip-installable distribution name for the provider's integration. + + Raises: + ValueError: If `provider` is not a built-in provider. + """ + try: + spec = _BUILTIN_PROVIDERS[provider] + except KeyError: + supported = ", ".join(sorted(_BUILTIN_PROVIDERS)) + msg = f"Unsupported {provider=}.\n\nSupported model providers are: {supported}" + raise ValueError(msg) from None + return spec.package + + +def _import_module(module: str, class_name: str, package: str | None = None) -> ModuleType: """Import a module by name. Args: module: The fully qualified module name to import (e.g., `'langchain_openai'`). class_name: The name of the class being imported, used for error messages. + package: The pip package that provides `module`, used in the install hint. + When `None`, it is derived from `module` (first segment, underscores + replaced by hyphens). Returns: The imported module. @@ -117,9 +178,8 @@ def _import_module(module: str, class_name: str) -> ModuleType: try: return importlib.import_module(module) except ImportError as e: - # Extract package name from module path (e.g., "langchain_azure_ai.chat_models" - # becomes "langchain-azure-ai") - pkg = module.split(".", maxsplit=1)[0].replace("_", "-") + # e.g. "langchain_azure_ai.chat_models" becomes "langchain-azure-ai" + pkg = package or module.split(".", maxsplit=1)[0].replace("_", "-") msg = ( f"Initializing {class_name} requires the {pkg} package. Please install it " f"with `pip install {pkg}`" @@ -153,22 +213,22 @@ def _get_chat_model_creator( msg = f"Unsupported {provider=}.\n\nSupported model providers are: {supported}" raise ValueError(msg) - pkg, class_name, creator_func = _BUILTIN_PROVIDERS[provider] + spec = _BUILTIN_PROVIDERS[provider] try: - module = _import_module(pkg, class_name) + module = _import_module(spec.module, spec.class_name, spec.package) except ImportError as e: if provider != "ollama": raise # For backwards compatibility try: - module = _import_module("langchain_community.chat_models", class_name) + module = _import_module("langchain_community.chat_models", spec.class_name) except ImportError: # If both langchain-ollama and langchain-community aren't available, # raise an error related to langchain-ollama raise e from None - cls = getattr(module, class_name) - return functools.partial(creator_func, cls=cls) + cls = getattr(module, spec.class_name) + return functools.partial(spec.creator, cls=cls) @overload diff --git a/libs/langchain_v1/langchain/embeddings/__init__.py b/libs/langchain_v1/langchain/embeddings/__init__.py index 3b0ab2982bc..00f51cc43d6 100644 --- a/libs/langchain_v1/langchain/embeddings/__init__.py +++ b/libs/langchain_v1/langchain/embeddings/__init__.py @@ -10,9 +10,10 @@ from langchain_core.embeddings import Embeddings -from langchain.embeddings.base import init_embeddings +from langchain.embeddings.base import get_provider_package, init_embeddings __all__ = [ "Embeddings", + "get_provider_package", "init_embeddings", ] diff --git a/libs/langchain_v1/langchain/embeddings/base.py b/libs/langchain_v1/langchain/embeddings/base.py index b488c35861a..4e09b059f84 100644 --- a/libs/langchain_v1/langchain/embeddings/base.py +++ b/libs/langchain_v1/langchain/embeddings/base.py @@ -3,7 +3,7 @@ import functools import importlib from collections.abc import Callable -from typing import Any +from typing import Any, NamedTuple from langchain_core.embeddings import Embeddings @@ -12,33 +12,65 @@ def _call(cls: type[Embeddings], **kwargs: Any) -> Embeddings: return cls(**kwargs) -_BUILTIN_PROVIDERS: dict[str, tuple[str, str, Callable[..., Embeddings]]] = { - "azure_ai": ("langchain_azure_ai.embeddings", "AzureAIOpenAIApiEmbeddingsModel", _call), - "azure_openai": ("langchain_openai", "AzureOpenAIEmbeddings", _call), - "bedrock": ( +class _ProviderSpec(NamedTuple): + """Import configuration for a built-in embeddings provider. + + This is a `NamedTuple` for backwards compatibility with the previous + `(module, class_name, creator)` tuple: the first three fields keep their + positions, so existing positional indexing (e.g. `spec[0]`) and iteration + continue to work. + """ + + module: str + """Python module path containing the embeddings class.""" + class_name: str + """Name of the embeddings class to import.""" + creator: Callable[..., Embeddings] + """Callable that instantiates the class with provided kwargs.""" + pypi_name: str | None = None + """PyPI distribution name, set only when it differs from the derived default. + + When `None`, `package` derives the name from `module` (the first module + segment with underscores replaced by hyphens), which holds for every LangChain + integration package. Set this explicitly for providers whose import module and + PyPI distribution name diverge. + """ + + @property + def package(self) -> str: + """PyPI distribution name that provides this provider's integration.""" + if self.pypi_name is not None: + return self.pypi_name + return self.module.split(".", maxsplit=1)[0].replace("_", "-") + + +_BUILTIN_PROVIDERS: dict[str, _ProviderSpec] = { + "azure_ai": _ProviderSpec( + "langchain_azure_ai.embeddings", "AzureAIOpenAIApiEmbeddingsModel", _call + ), + "azure_openai": _ProviderSpec("langchain_openai", "AzureOpenAIEmbeddings", _call), + "bedrock": _ProviderSpec( "langchain_aws", "BedrockEmbeddings", lambda cls, model, **kwargs: cls(model_id=model, **kwargs), ), - "cohere": ("langchain_cohere", "CohereEmbeddings", _call), - "google_genai": ("langchain_google_genai", "GoogleGenerativeAIEmbeddings", _call), - "google_vertexai": ("langchain_google_vertexai", "VertexAIEmbeddings", _call), - "huggingface": ( + "cohere": _ProviderSpec("langchain_cohere", "CohereEmbeddings", _call), + "google_genai": _ProviderSpec("langchain_google_genai", "GoogleGenerativeAIEmbeddings", _call), + "google_vertexai": _ProviderSpec("langchain_google_vertexai", "VertexAIEmbeddings", _call), + "huggingface": _ProviderSpec( "langchain_huggingface", "HuggingFaceEmbeddings", lambda cls, model, **kwargs: cls(model_name=model, **kwargs), ), - "mistralai": ("langchain_mistralai", "MistralAIEmbeddings", _call), - "ollama": ("langchain_ollama", "OllamaEmbeddings", _call), - "openai": ("langchain_openai", "OpenAIEmbeddings", _call), + "mistralai": _ProviderSpec("langchain_mistralai", "MistralAIEmbeddings", _call), + "ollama": _ProviderSpec("langchain_ollama", "OllamaEmbeddings", _call), + "openai": _ProviderSpec("langchain_openai", "OpenAIEmbeddings", _call), } """Registry mapping provider names to their import configuration. -Each entry maps a provider key to a tuple of: - -- `module_path`: The Python module path containing the embeddings class. -- `class_name`: The name of the embeddings class to import. -- `creator_func`: A callable that instantiates the class with provided kwargs. +Each entry maps a provider key to a `_ProviderSpec`, holding the import module +path, the embeddings class name, a callable that instantiates it, and an optional +explicit PyPI distribution name (see `_ProviderSpec.package`). !!! note @@ -81,23 +113,47 @@ def _get_embeddings_class_creator(provider: str) -> Callable[..., Embeddings]: ) raise ValueError(msg) - module_name, class_name, creator_func = _BUILTIN_PROVIDERS[provider] + spec = _BUILTIN_PROVIDERS[provider] try: - module = importlib.import_module(module_name) + module = importlib.import_module(spec.module) except ImportError as e: - pkg = module_name.split(".", maxsplit=1)[0].replace("_", "-") + pkg = spec.package msg = f"Could not import {pkg} python package. Please install it with `pip install {pkg}`" raise ImportError(msg) from e - cls = getattr(module, class_name) - return functools.partial(creator_func, cls=cls) + cls = getattr(module, spec.class_name) + return functools.partial(spec.creator, cls=cls) + + +def get_provider_package(provider: str) -> str: + """Return the PyPI package that provides a built-in provider's integration. + + Use this to map a provider key (e.g. `'openai'`) to the pip-installable + distribution name (e.g. `'langchain-openai'`) without hard-coding the mapping + or re-deriving it from module paths. + + Args: + provider: A built-in provider key, as accepted by `init_embeddings`'s + `provider` argument (e.g. `'openai'`, `'cohere'`). + + Returns: + The pip-installable distribution name for the provider's integration. + + Raises: + ValueError: If `provider` is not a built-in provider. + """ + try: + spec = _BUILTIN_PROVIDERS[provider] + except KeyError: + supported = ", ".join(sorted(_BUILTIN_PROVIDERS)) + msg = f"Unsupported {provider=}.\n\nSupported model providers are: {supported}" + raise ValueError(msg) from None + return spec.package def _get_provider_list() -> str: """Get formatted list of providers and their packages.""" - return "\n".join( - f" - {p}: {pkg[0].replace('_', '-')}" for p, pkg in _BUILTIN_PROVIDERS.items() - ) + return "\n".join(f" - {p}: {spec.package}" for p, spec in _BUILTIN_PROVIDERS.items()) def _parse_model_string(model_name: str) -> tuple[str, str]: diff --git a/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py b/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py index 0282c3e307d..20ed36355ba 100644 --- a/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py +++ b/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py @@ -8,7 +8,7 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnableConfig, RunnableSequence from pydantic import SecretStr -from langchain.chat_models import __all__, init_chat_model +from langchain.chat_models import __all__, get_provider_package, init_chat_model from langchain.chat_models.base import _BUILTIN_PROVIDERS, _attempt_infer_model_provider if TYPE_CHECKING: @@ -19,6 +19,7 @@ OPENAI_TEST_MODEL = "gpt-5.5" EXPECTED_ALL = [ "init_chat_model", "BaseChatModel", + "get_provider_package", ] @@ -76,6 +77,28 @@ def test_supported_providers_is_sorted() -> None: assert list(_BUILTIN_PROVIDERS) == sorted(_BUILTIN_PROVIDERS.keys()) +def test_get_provider_package() -> None: + """The accessor returns the pip package name for a provider.""" + assert get_provider_package("openai") == "langchain-openai" + assert get_provider_package("anthropic") == "langchain-anthropic" + # Submodule import paths resolve to the top-level distribution. + assert get_provider_package("azure_ai") == "langchain-azure-ai" + assert get_provider_package("nvidia") == "langchain-nvidia-ai-endpoints" + + +def test_get_provider_package_matches_registry() -> None: + """Every provider resolves to a derived name unless a pypi_name is set.""" + for provider, spec in _BUILTIN_PROVIDERS.items(): + expected = spec.pypi_name or spec.module.split(".", 1)[0].replace("_", "-") + assert get_provider_package(provider) == expected + + +def test_get_provider_package_unknown() -> None: + """Unknown providers raise a helpful error.""" + with pytest.raises(ValueError, match="Unsupported provider='bar'"): + get_provider_package("bar") + + @pytest.mark.parametrize( ("model_name", "expected_provider"), [ diff --git a/libs/langchain_v1/tests/unit_tests/embeddings/test_base.py b/libs/langchain_v1/tests/unit_tests/embeddings/test_base.py index 901a1138a28..fabb59abe4e 100644 --- a/libs/langchain_v1/tests/unit_tests/embeddings/test_base.py +++ b/libs/langchain_v1/tests/unit_tests/embeddings/test_base.py @@ -6,6 +6,7 @@ from langchain.embeddings.base import ( _BUILTIN_PROVIDERS, _infer_model_and_provider, _parse_model_string, + get_provider_package, ) @@ -110,3 +111,22 @@ def test_supported_providers_package_names(provider: str) -> None: def test_is_sorted() -> None: assert list(_BUILTIN_PROVIDERS) == sorted(_BUILTIN_PROVIDERS.keys()) + + +def test_get_provider_package() -> None: + """The accessor returns the pip package name for a provider.""" + assert get_provider_package("openai") == "langchain-openai" + assert get_provider_package("azure_ai") == "langchain-azure-ai" + + +def test_get_provider_package_matches_registry() -> None: + """Every provider resolves to a derived name unless a pypi_name is set.""" + for provider, spec in _BUILTIN_PROVIDERS.items(): + expected = spec.pypi_name or spec.module.split(".", 1)[0].replace("_", "-") + assert get_provider_package(provider) == expected + + +def test_get_provider_package_unknown() -> None: + """Unknown providers raise a helpful error.""" + with pytest.raises(ValueError, match="Unsupported provider='bar'"): + get_provider_package("bar")