diff --git a/CHANGELOG.md b/CHANGELOG.md index 420dc29..3a9894d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `uipath_llm_client` (core package) will be documented in this file. +## [1.14.0] - 2026-06-15 + +### Added +- `ApiFlavor.ANTHROPIC_MESSAGES` (`"AnthropicMessages"`) for Bedrock-hosted Claude models that the discovery endpoint exposes with the native Anthropic Messages wire format. The value is the discovery string verbatim because it is forwarded as the `X-UiPath-LlmGateway-ApiFlavor` header. Also mapped in `API_FLAVOR_TO_VENDOR_TYPE` to `VendorType.AWSBEDROCK` so it resolves to the Bedrock passthrough route when discovery omits the vendor field. + ## [1.13.1] - 2026-06-09 ### Fixed diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index 2739ff3..e6f40ae 100644 --- a/packages/uipath_langchain_client/CHANGELOG.md +++ b/packages/uipath_langchain_client/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `uipath_langchain_client` will be documented in this file. +## [1.14.0] - 2026-06-15 + +### Added +- Support for the `AnthropicMessages` API flavor on Bedrock-hosted Claude models. `get_chat_model` now routes discovery's `apiFlavor=AnthropicMessages` (vendor `AwsBedrock`) to `UiPathChatAnthropic` configured with `vendor_type=awsbedrock` and `api_flavor=ApiFlavor.ANTHROPIC_MESSAGES`. The client keeps the Bedrock passthrough URL but uses the native `Anthropic`/`AsyncAnthropic` SDK (model-in-body wire format), which the gateway requires for this flavor, instead of `AnthropicBedrock`. +- `UiPathChatAnthropic` now accepts an explicit `api_flavor`. When set to `ApiFlavor.ANTHROPIC_MESSAGES` it selects the native Anthropic SDK regardless of `vendor_type`; otherwise the flavor and SDK are derived from `vendor_type` exactly as before (`awsbedrock` → `invoke` + `AnthropicBedrock`, unchanged). + +### Changed +- Bumped `uipath-llm-client` floor to `>=1.14.0` to pick up `ApiFlavor.ANTHROPIC_MESSAGES`. + ## [1.13.1] - 2026-06-09 ### Fixed diff --git a/packages/uipath_langchain_client/pyproject.toml b/packages/uipath_langchain_client/pyproject.toml index d640dad..3261248 100644 --- a/packages/uipath_langchain_client/pyproject.toml +++ b/packages/uipath_langchain_client/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "langchain>=1.2.15,<2.0.0", - "uipath-llm-client>=1.13.1,<2.0.0", + "uipath-llm-client>=1.14.0,<2.0.0", ] [project.optional-dependencies] 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 6cdfc11..6e980c9 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.13.1" +__version__ = "1.14.0" diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/anthropic/chat_models.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/anthropic/chat_models.py index 26b87a2..c1c8d92 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/anthropic/chat_models.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/anthropic/chat_models.py @@ -40,10 +40,20 @@ class UiPathChatAnthropic(UiPathBaseChatModel, ChatAnthropic): freeze_base_url=True, ) vendor_type: VendorType = VendorType.ANTHROPIC + api_flavor: ApiFlavor | str | None = None + """Explicit API flavor. When ``ApiFlavor.ANTHROPIC_MESSAGES``, the request + uses the native Anthropic Messages wire format (model in body) while routing + through the hosting vendor's passthrough URL (e.g. ``vendor_type=awsbedrock`` + for a Bedrock-hosted Claude). Otherwise the flavor is derived from + ``vendor_type``.""" @model_validator(mode="after") def setup_api_flavor_and_version(self) -> Self: self.api_config.vendor_type = self.vendor_type + match self.api_flavor: + case ApiFlavor.ANTHROPIC_MESSAGES: + self.api_config.api_flavor = ApiFlavor.ANTHROPIC_MESSAGES + return self match self.vendor_type: case VendorType.ANTHROPIC: self.api_config.api_flavor = None @@ -67,6 +77,15 @@ def setup_api_flavor_and_version(self) -> Self: def _anthropic_client( self, ) -> Anthropic | AnthropicVertex | AnthropicBedrock | AnthropicFoundry: + match self.api_config.api_flavor: + case ApiFlavor.ANTHROPIC_MESSAGES: + return Anthropic( + api_key="PLACEHOLDER", + base_url=str(self.uipath_sync_client.base_url), + default_headers=dict(self.uipath_sync_client.headers), + max_retries=0, # handled by the UiPathBaseChatModel + http_client=self.uipath_sync_client, + ) match self.vendor_type: case VendorType.ANTHROPIC: return Anthropic( @@ -111,6 +130,15 @@ def _anthropic_client( def _async_anthropic_client( self, ) -> AsyncAnthropic | AsyncAnthropicVertex | AsyncAnthropicBedrock | AsyncAnthropicFoundry: + match self.api_config.api_flavor: + case ApiFlavor.ANTHROPIC_MESSAGES: + return AsyncAnthropic( + api_key="PLACEHOLDER", + base_url=str(self.uipath_async_client.base_url), + default_headers=dict(self.uipath_async_client.headers), + max_retries=0, # handled by the UiPathBaseChatModel + http_client=self.uipath_async_client, + ) match self.vendor_type: case VendorType.ANTHROPIC: return AsyncAnthropic( 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 6982068..39b438b 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py @@ -190,6 +190,21 @@ def get_chat_model( **model_kwargs, ) case VendorType.AWSBEDROCK: + if api_flavor == ApiFlavor.ANTHROPIC_MESSAGES: + from uipath_langchain_client.clients.anthropic.chat_models import ( + UiPathChatAnthropic, + ) + + return UiPathChatAnthropic( + model=model_name, + settings=client_settings, + vendor_type=VendorType.AWSBEDROCK, + api_flavor=ApiFlavor.ANTHROPIC_MESSAGES, + byo_connection_id=byo_connection_id, + model_details=model_details, + **model_kwargs, + ) + if api_flavor == ApiFlavor.INVOKE: if model_family == ModelFamily.ANTHROPIC_CLAUDE: from uipath_langchain_client.clients.bedrock.chat_models import ( diff --git a/src/uipath/llm_client/__version__.py b/src/uipath/llm_client/__version__.py index d206086..fdf225c 100644 --- a/src/uipath/llm_client/__version__.py +++ b/src/uipath/llm_client/__version__.py @@ -1,3 +1,3 @@ __title__ = "UiPath LLM Client" __description__ = "A Python client for interacting with UiPath's LLM services." -__version__ = "1.13.1" +__version__ = "1.14.0" diff --git a/src/uipath/llm_client/clients/litellm/client.py b/src/uipath/llm_client/clients/litellm/client.py index cfa4f9a..c63941e 100644 --- a/src/uipath/llm_client/clients/litellm/client.py +++ b/src/uipath/llm_client/clients/litellm/client.py @@ -99,6 +99,7 @@ ByomApiFlavor.GEMINI_EMBEDDINGS: "gemini", ByomApiFlavor.AWS_BEDROCK_INVOKE: "bedrock", ByomApiFlavor.AWS_BEDROCK_CONVERSE: "bedrock", + ByomApiFlavor.ANTHROPIC_MESSAGES: "bedrock", } diff --git a/src/uipath/llm_client/settings/constants.py b/src/uipath/llm_client/settings/constants.py index 6f073b7..0493e9e 100644 --- a/src/uipath/llm_client/settings/constants.py +++ b/src/uipath/llm_client/settings/constants.py @@ -32,6 +32,7 @@ class ApiFlavor(StrEnum): CONVERSE = "converse" INVOKE = "invoke" ANTHROPIC_CLAUDE = "anthropic-claude" + ANTHROPIC_MESSAGES = "AnthropicMessages" class ByomApiFlavor(StrEnum): @@ -44,6 +45,7 @@ class ByomApiFlavor(StrEnum): GEMINI_EMBEDDINGS = "GeminiEmbeddings" AWS_BEDROCK_INVOKE = "AwsBedrockInvoke" AWS_BEDROCK_CONVERSE = "AwsBedrockConverse" + ANTHROPIC_MESSAGES = "AnthropicMessages" API_FLAVOR_TO_VENDOR_TYPE: dict[str, VendorType] = { @@ -53,6 +55,7 @@ class ByomApiFlavor(StrEnum): ApiFlavor.ANTHROPIC_CLAUDE: VendorType.VERTEXAI, ApiFlavor.CONVERSE: VendorType.AWSBEDROCK, ApiFlavor.INVOKE: VendorType.AWSBEDROCK, + ByomApiFlavor.ANTHROPIC_MESSAGES: VendorType.AWSBEDROCK, ByomApiFlavor.OPENAI_CHAT_COMPLETIONS: VendorType.OPENAI, ByomApiFlavor.OPENAI_RESPONSES: VendorType.OPENAI, ByomApiFlavor.OPENAI_EMBEDDINGS: VendorType.OPENAI, @@ -69,4 +72,5 @@ class ByomApiFlavor(StrEnum): ByomApiFlavor.GEMINI_GENERATE_CONTENT: ApiFlavor.GENERATE_CONTENT, ByomApiFlavor.AWS_BEDROCK_INVOKE: ApiFlavor.INVOKE, ByomApiFlavor.AWS_BEDROCK_CONVERSE: ApiFlavor.CONVERSE, + ByomApiFlavor.ANTHROPIC_MESSAGES: ApiFlavor.ANTHROPIC_MESSAGES, } diff --git a/tests/core/features/test_api_config.py b/tests/core/features/test_api_config.py index 9454207..2f9c23e 100644 --- a/tests/core/features/test_api_config.py +++ b/tests/core/features/test_api_config.py @@ -3,7 +3,13 @@ import pytest from uipath.llm_client.settings import UiPathAPIConfig -from uipath.llm_client.settings.constants import ApiFlavor, ApiType, RoutingMode, VendorType +from uipath.llm_client.settings.constants import ( + API_FLAVOR_TO_VENDOR_TYPE, + ApiFlavor, + ApiType, + RoutingMode, + VendorType, +) class TestUiPathAPIConfig: @@ -83,6 +89,10 @@ def test_api_flavor_values(self): assert ApiFlavor.CONVERSE == "converse" assert ApiFlavor.INVOKE == "invoke" assert ApiFlavor.ANTHROPIC_CLAUDE == "anthropic-claude" + assert ApiFlavor.ANTHROPIC_MESSAGES == "AnthropicMessages" + + def test_anthropic_messages_maps_to_bedrock_vendor(self): + assert API_FLAVOR_TO_VENDOR_TYPE[ApiFlavor.ANTHROPIC_MESSAGES] == VendorType.AWSBEDROCK def test_enum_string_comparison(self): assert ApiType.COMPLETIONS == "completions" diff --git a/tests/langchain/clients/anthropic/test_unit.py b/tests/langchain/clients/anthropic/test_unit.py index 9422f9b..335b854 100644 --- a/tests/langchain/clients/anthropic/test_unit.py +++ b/tests/langchain/clients/anthropic/test_unit.py @@ -3,11 +3,17 @@ from typing import Any import pytest +from anthropic import ( + Anthropic, + AnthropicBedrock, + AsyncAnthropic, + AsyncAnthropicBedrock, +) from langchain_core.language_models.chat_models import BaseChatModel from langchain_tests.unit_tests import ChatModelUnitTests from uipath_langchain_client.clients.anthropic.chat_models import UiPathChatAnthropic -from uipath.llm_client.settings import UiPathBaseSettings +from uipath.llm_client.settings import ApiFlavor, UiPathBaseSettings, VendorType ANTHROPIC_CHAT_CLASSES = [UiPathChatAnthropic] @@ -32,3 +38,67 @@ def chat_model_params(self) -> dict[str, Any]: @pytest.mark.xfail(reason="Skipping serdes test for now") def test_serdes(self, *args: Any, **kwargs: Any) -> None: ... + + +def _build(client_settings: UiPathBaseSettings, **kwargs: Any) -> UiPathChatAnthropic: + return UiPathChatAnthropic( + model="anthropic.claude-sonnet-4-6", + settings=client_settings, + model_details={}, + **kwargs, + ) + + +class TestAnthropicMessagesFlavor: + """AnthropicMessages uses the native Anthropic SDK (model-in-body wire format) + over the awsbedrock passthrough URL.""" + + def test_sets_anthropic_messages_flavor_over_bedrock_url( + self, client_settings: UiPathBaseSettings + ): + chat = _build( + client_settings, + vendor_type=VendorType.AWSBEDROCK, + api_flavor=ApiFlavor.ANTHROPIC_MESSAGES, + ) + assert chat.api_config.vendor_type == VendorType.AWSBEDROCK + assert chat.api_config.api_flavor == ApiFlavor.ANTHROPIC_MESSAGES + + def test_uses_native_anthropic_sdk(self, client_settings: UiPathBaseSettings): + chat = _build( + client_settings, + vendor_type=VendorType.AWSBEDROCK, + api_flavor=ApiFlavor.ANTHROPIC_MESSAGES, + ) + assert isinstance(chat._anthropic_client, Anthropic) + assert not isinstance(chat._anthropic_client, AnthropicBedrock) + assert isinstance(chat._async_anthropic_client, AsyncAnthropic) + assert not isinstance(chat._async_anthropic_client, AsyncAnthropicBedrock) + + def test_flavor_is_orthogonal_to_vendor_type(self, client_settings: UiPathBaseSettings): + chat = _build( + client_settings, + vendor_type=VendorType.ANTHROPIC, + api_flavor=ApiFlavor.ANTHROPIC_MESSAGES, + ) + assert chat.api_config.api_flavor == ApiFlavor.ANTHROPIC_MESSAGES + assert isinstance(chat._anthropic_client, Anthropic) + + +class TestVendorDerivedDefaultsUnchanged: + """Regression guard: omitting api_flavor preserves the prior vendor-derived behavior.""" + + def test_bedrock_without_flavor_uses_invoke_and_anthropic_bedrock( + self, client_settings: UiPathBaseSettings + ): + chat = _build(client_settings, vendor_type=VendorType.AWSBEDROCK) + assert chat.api_config.api_flavor == ApiFlavor.INVOKE + assert isinstance(chat._anthropic_client, AnthropicBedrock) + assert isinstance(chat._async_anthropic_client, AsyncAnthropicBedrock) + + def test_anthropic_vendor_defaults_to_anthropic_client( + self, client_settings: UiPathBaseSettings + ): + chat = _build(client_settings, vendor_type=VendorType.ANTHROPIC) + assert isinstance(chat._anthropic_client, Anthropic) + assert not isinstance(chat._anthropic_client, AnthropicBedrock) diff --git a/tests/langchain/features/test_factory_function.py b/tests/langchain/features/test_factory_function.py index cc507cb..1147954 100644 --- a/tests/langchain/features/test_factory_function.py +++ b/tests/langchain/features/test_factory_function.py @@ -6,7 +6,7 @@ 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 ApiFlavor, UiPathBaseSettings +from uipath.llm_client.settings import ApiFlavor, UiPathBaseSettings, VendorType @pytest.mark.vcr @@ -333,3 +333,49 @@ def test_no_kwarg_keeps_settings_value(self, monkeypatch: pytest.MonkeyPatch): ) assert captured["settings"] is original original.model_copy.assert_not_called() + + +class TestFactoryAnthropicMessagesRouting: + """AwsBedrock + ``apiFlavor=AnthropicMessages`` routes to ``UiPathChatAnthropic`` + configured for the native Anthropic Messages wire format over the Bedrock + passthrough URL (not the Bedrock Converse/Invoke clients).""" + + def _capture( + 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.anthropic.chat_models.UiPathChatAnthropic", + _StubModel, + ) + get_chat_model( + model_name=model_info["modelName"], + client_settings=settings, + **factory_kwargs, + ) + return captured + + def test_anthropic_messages_routes_to_uipath_chat_anthropic( + self, monkeypatch: pytest.MonkeyPatch + ): + captured = self._capture( + monkeypatch, + { + "modelName": "anthropic.claude-sonnet-4-6", + "vendor": "AwsBedrock", + "apiFlavor": "AnthropicMessages", + "modelFamily": "AnthropicClaude", + }, + ) + assert captured["vendor_type"] == VendorType.AWSBEDROCK + assert captured["api_flavor"] == ApiFlavor.ANTHROPIC_MESSAGES