diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index 3061ab8..aefc027 100644 --- a/packages/uipath_langchain_client/CHANGELOG.md +++ b/packages/uipath_langchain_client/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `uipath_langchain_client` will be documented in this file. +## [1.9.3] - 2026-04-20 + +### Changed +- `get_chat_model()` now defaults to the OpenAI Responses API (`ApiFlavor.RESPONSES`) when discovery does not specify a flavor for an OpenAI chat model. Explicit `api_flavor=` on the call and BYOM-discovered flavors still take precedence. The LiteLLM client still defaults to chat-completions for OpenAI because LiteLLM 1.83.x drops the injected httpx `client` when its acompletion→aresponses bridge fires, which breaks async auth against the UiPath gateway. + ## [1.9.2] - 2026-04-17 ### Changed diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py index 84b41a2..e11ded6 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py @@ -1,3 +1,3 @@ __title__ = "UiPath LangChain Client" __description__ = "A Python client for interacting with UiPath's LLM services via LangChain." -__version__ = "1.9.2" +__version__ = "1.9.3" diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py b/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py index de491ff..8ef3b99 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py @@ -124,6 +124,10 @@ def get_chat_model( match discovered_vendor_type: case VendorType.OPENAI: + # OpenAI chat defaults to the Responses API when no flavor is specified. + if api_flavor is None: + api_flavor = ApiFlavor.RESPONSES + if model_family == ModelFamily.OPENAI: from uipath_langchain_client.clients.openai.chat_models import ( UiPathAzureChatOpenAI, diff --git a/tests/cassettes.db b/tests/cassettes.db index 748ecb9..bbd58c8 100644 Binary files a/tests/cassettes.db and b/tests/cassettes.db differ diff --git a/tests/langchain/features/test_factory_function.py b/tests/langchain/features/test_factory_function.py index 6ccdb6e..3677157 100644 --- a/tests/langchain/features/test_factory_function.py +++ b/tests/langchain/features/test_factory_function.py @@ -1,10 +1,12 @@ +from unittest.mock import MagicMock + import pytest from uipath_langchain_client.clients.normalized.chat_models import UiPathChat from uipath_langchain_client.clients.normalized.embeddings import UiPathEmbeddings from uipath_langchain_client.factory import get_chat_model, get_embedding_model from tests.langchain.conftest import COMPLETION_MODEL_NAMES, EMBEDDING_MODEL_NAMES -from uipath.llm_client.settings import UiPathBaseSettings +from uipath.llm_client.settings import ApiFlavor, UiPathBaseSettings @pytest.mark.vcr @@ -44,3 +46,85 @@ def test_get_embedding_model_custom_class( ) assert embedding_model is not None assert isinstance(embedding_model, UiPathEmbeddings) + + +class TestFactoryDefaultApiFlavor: + """Unit tests for the default api_flavor picked by the chat factory. + + The factory returns concrete LangChain model classes whose construction is + non-trivial. Instead of fully instantiating them, we patch the concrete + classes with a sentinel that captures the kwargs the factory passes. + """ + + def _captured_kwargs( + self, + monkeypatch: pytest.MonkeyPatch, + model_info: dict, + **factory_kwargs, + ) -> dict: + settings = MagicMock() + settings.get_model_info.return_value = model_info + captured: dict = {} + + class _StubModel: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr( + "uipath_langchain_client.clients.openai.chat_models.UiPathChatOpenAI", + _StubModel, + ) + monkeypatch.setattr( + "uipath_langchain_client.clients.openai.chat_models.UiPathAzureChatOpenAI", + _StubModel, + ) + get_chat_model( + model_name=model_info["modelName"], + client_settings=settings, + **factory_kwargs, + ) + return captured + + def test_openai_chat_defaults_to_responses_when_no_flavor_discovered( + self, monkeypatch: pytest.MonkeyPatch + ): + """UiPath-owned OpenAI (apiFlavor=null) should default to the Responses API.""" + captured = self._captured_kwargs( + monkeypatch, + { + "modelName": "gpt-4o", + "vendor": "OpenAi", + "apiFlavor": None, + "modelFamily": "OpenAi", + }, + ) + assert captured["api_flavor"] == ApiFlavor.RESPONSES + + def test_openai_chat_respects_user_api_flavor_override(self, monkeypatch: pytest.MonkeyPatch): + """Explicit api_flavor from the caller still wins over the default.""" + captured = self._captured_kwargs( + monkeypatch, + { + "modelName": "gpt-4o", + "vendor": "OpenAi", + "apiFlavor": None, + "modelFamily": "OpenAi", + }, + api_flavor=ApiFlavor.CHAT_COMPLETIONS, + ) + assert captured["api_flavor"] == ApiFlavor.CHAT_COMPLETIONS + + def test_openai_chat_respects_discovered_byom_chat_completions( + self, monkeypatch: pytest.MonkeyPatch + ): + """BYOM-discovered chat-completions still maps to chat-completions.""" + captured = self._captured_kwargs( + monkeypatch, + { + "modelName": "custom-gpt", + "vendor": "OpenAi", + "apiFlavor": "OpenAiChatCompletions", + "modelFamily": None, + }, + ) + assert captured["api_flavor"] == ApiFlavor.CHAT_COMPLETIONS