From 24bb15c89305be266bbc9b43380bb398f773e89a Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Wed, 10 Jun 2026 17:57:45 +0300 Subject: [PATCH] fix: resolve real bedrock backing model for BYO aliases in factory --- packages/uipath_langchain_client/CHANGELOG.md | 5 + .../uipath_langchain_client/__version__.py | 2 +- .../clients/bedrock/chat_models.py | 20 ++-- .../clients/bedrock/model_resolution.py | 53 ++++++++++ .../src/uipath_langchain_client/factory.py | 31 +++--- .../clients/bedrock/test_model_resolution.py | 85 +++++++++++++++ .../features/test_factory_function.py | 100 +++++++++++++++++- 7 files changed, 271 insertions(+), 25 deletions(-) create mode 100644 packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/model_resolution.py create mode 100644 tests/langchain/clients/bedrock/test_model_resolution.py diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index 991543b6..2b45f976 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.15.1] - 2026-06-30 + +### Fixed +- Bedrock clients now determine the provider and base model from the underlying customer model, in the case of BYOM configurations. This fixes a bug where agents with Bedrock BYOM configurations with custom aliases would fail to run. + ## [1.15.0] - 2026-06-25 ### Added 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 b33880d1..2e858bcd 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.15.0" +__version__ = "1.15.1" diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/chat_models.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/chat_models.py index 5df1cf2d..e0b45bb3 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/chat_models.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/chat_models.py @@ -48,6 +48,14 @@ def _patched_format_data_content_block(block: dict) -> dict: ) from e +def _setup_model_id(values: Any) -> Any: + if isinstance(values, dict) and "model_id" not in values: + model = values.get("model") or values.get("model_name") + if model: + values = {**values, "model_id": model} + return values + + class UiPathChatBedrockConverse(UiPathBaseChatModel, ChatBedrockConverse): # type: ignore[override] api_config: UiPathAPIConfig = UiPathAPIConfig( api_type=ApiType.COMPLETIONS, @@ -65,11 +73,7 @@ class UiPathChatBedrockConverse(UiPathBaseChatModel, ChatBedrockConverse): # ty @model_validator(mode="before") @classmethod def setup_model_id(cls, values: Any) -> Any: - if isinstance(values, dict) and "model_id" not in values: - model = values.get("model") or values.get("model_name") - if model: - values = {**values, "model_id": model} - return values + return _setup_model_id(values) @model_validator(mode="after") def setup_uipath_client(self) -> Self: @@ -94,11 +98,7 @@ class UiPathChatBedrock(UiPathBaseChatModel, ChatBedrock): # type: ignore[overr @model_validator(mode="before") @classmethod def setup_model_id(cls, values: Any) -> Any: - if isinstance(values, dict) and "model_id" not in values: - model = values.get("model") or values.get("model_name") - if model: - values = {**values, "model_id": model} - return values + return _setup_model_id(values) @model_validator(mode="after") def setup_uipath_client(self) -> Self: diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/model_resolution.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/model_resolution.py new file mode 100644 index 00000000..056c4835 --- /dev/null +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/model_resolution.py @@ -0,0 +1,53 @@ +"""Bedrock backing-model and provider resolution.""" + +from typing import Any + +_BEDROCK_REGIONS = ( + "eu", + "us", + "us-gov", + "apac", + "sa", + "amer", + "global", + "jp", + "au", +) + + +def _split_model_id(model_id: str) -> tuple[str, str] | None: + model_id = model_id.strip() + parts = model_id.split(".") + + if parts[0] in _BEDROCK_REGIONS: + parts = parts[1:] + + if len(parts) < 2: + return None + + provider = parts[0] + if not provider or " " in provider or "/" in provider: + return None + + return model_id, provider + + +def _resolve_backing_model(model_info: dict[str, Any]) -> tuple[str, str] | None: + byo_details = model_info.get("byomDetails") or {} + customer_model = byo_details.get("customerModel") + if isinstance(customer_model, str): + return _split_model_id(customer_model) + return None + + +def apply_backing_model_detection_hints( + model_kwargs: dict[str, Any], model_info: dict[str, Any] +) -> None: + if "base_model_id" in model_kwargs or "base_model" in model_kwargs: + return + backing = _resolve_backing_model(model_info) + if not backing: + return + base_model_id, provider = backing + model_kwargs["base_model_id"] = base_model_id + model_kwargs.setdefault("provider", provider) 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 39b438b7..a7e36be8 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/factory.py @@ -26,6 +26,9 @@ UiPathBaseChatModel, UiPathBaseEmbeddings, ) +from uipath_langchain_client.clients.bedrock.model_resolution import ( + apply_backing_model_detection_hints, +) from uipath_langchain_client.settings import ( API_FLAVOR_TO_VENDOR_TYPE, BYOM_TO_ROUTING_FLAVOR, @@ -205,20 +208,22 @@ def get_chat_model( **model_kwargs, ) - if api_flavor == ApiFlavor.INVOKE: - if model_family == ModelFamily.ANTHROPIC_CLAUDE: - from uipath_langchain_client.clients.bedrock.chat_models import ( - UiPathChatAnthropicBedrock, - ) - - return UiPathChatAnthropicBedrock( - model=model_name, - settings=client_settings, - byo_connection_id=byo_connection_id, - model_details=model_details, - **model_kwargs, - ) + if api_flavor == ApiFlavor.INVOKE and model_family == ModelFamily.ANTHROPIC_CLAUDE: + from uipath_langchain_client.clients.bedrock.chat_models import ( + UiPathChatAnthropicBedrock, + ) + return UiPathChatAnthropicBedrock( + model=model_name, + settings=client_settings, + byo_connection_id=byo_connection_id, + model_details=model_details, + **model_kwargs, + ) + + apply_backing_model_detection_hints(model_kwargs, model_info) + + if api_flavor == ApiFlavor.INVOKE: from uipath_langchain_client.clients.bedrock.chat_models import ( UiPathChatBedrock, ) diff --git a/tests/langchain/clients/bedrock/test_model_resolution.py b/tests/langchain/clients/bedrock/test_model_resolution.py new file mode 100644 index 00000000..4ab5f01c --- /dev/null +++ b/tests/langchain/clients/bedrock/test_model_resolution.py @@ -0,0 +1,85 @@ +"""Unit tests for Bedrock backing-model resolution.""" + +import pytest +from uipath_langchain_client.clients.bedrock.model_resolution import ( + apply_backing_model_detection_hints, +) + + +class TestApplyBackingModelDetectionHints: + @pytest.mark.parametrize( + "customer_model,expected_provider", + [ + ("anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic"), + ("global.anthropic.claude-sonnet-4-6", "anthropic"), + ("amazon.nova-pro-v1:0", "amazon"), + ], + ) + def test_byo_uses_customer_model(self, customer_model, expected_provider): + kwargs: dict = {} + apply_backing_model_detection_hints( + kwargs, + { + "modelName": "AWS - Bedrock", + "byomDetails": { + "customerModel": customer_model, + "integrationServiceConnectionId": "conn-x", + }, + }, + ) + assert kwargs["base_model_id"] == customer_model + assert kwargs["provider"] == expected_provider + + def test_unparseable_customer_model_sets_no_hints(self): + kwargs: dict = {} + apply_backing_model_detection_hints( + kwargs, + { + "modelName": "AWS - Bedrock", + "byomDetails": {"customerModel": "my-claude-sonnet-4-5"}, + }, + ) + assert "base_model_id" not in kwargs + assert "provider" not in kwargs + + def test_non_byo_model_sets_no_hints(self): + kwargs: dict = {} + apply_backing_model_detection_hints( + kwargs, + { + "modelName": "anthropic.claude-3-5-sonnet-20240620-v1:0", + "byomDetails": None, + }, + ) + assert "base_model_id" not in kwargs + assert "provider" not in kwargs + + def test_byo_alias_without_customer_model_sets_no_hints(self): + kwargs: dict = {} + apply_backing_model_detection_hints( + kwargs, + { + "modelName": "VeryCustomBedddrockAlias", + "byomDetails": {"integrationServiceConnectionId": "conn-x"}, + }, + ) + assert "base_model_id" not in kwargs + assert "provider" not in kwargs + + def test_does_not_override_caller_supplied_hints(self): + kwargs = {"base_model_id": "amazon.nova-pro-v1:0", "provider": "amazon"} + apply_backing_model_detection_hints( + kwargs, + {"byomDetails": {"customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0"}}, + ) + assert kwargs["base_model_id"] == "amazon.nova-pro-v1:0" + assert kwargs["provider"] == "amazon" + + def test_caller_base_model_alias_is_not_shadowed_by_base_model_id(self): + kwargs = {"base_model": "amazon.nova-pro-v1:0"} + apply_backing_model_detection_hints( + kwargs, + {"byomDetails": {"customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0"}}, + ) + assert "base_model_id" not in kwargs + assert kwargs["base_model"] == "amazon.nova-pro-v1:0" diff --git a/tests/langchain/features/test_factory_function.py b/tests/langchain/features/test_factory_function.py index 11479548..566e28d6 100644 --- a/tests/langchain/features/test_factory_function.py +++ b/tests/langchain/features/test_factory_function.py @@ -1,9 +1,16 @@ from unittest.mock import MagicMock import pytest +from uipath_langchain_client.clients.bedrock.chat_models import ( + UiPathChatBedrock, + UiPathChatBedrockConverse, +) 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 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, VendorType @@ -379,3 +386,94 @@ def test_anthropic_messages_routes_to_uipath_chat_anthropic( ) assert captured["vendor_type"] == VendorType.AWSBEDROCK assert captured["api_flavor"] == ApiFlavor.ANTHROPIC_MESSAGES + + +_BYO_BEDROCK_CONVERSE = { + "modelName": "AWS - Bedrock", + "vendor": "AwsBedrock", + "modelFamily": None, + "apiFlavor": None, + "modelSubscriptionType": "BYOMAdded", + "byomDetails": { + "customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0", + "integrationServiceConnectionId": "conn-x", + }, +} +_BYO_BEDROCK_INVOKE = {**_BYO_BEDROCK_CONVERSE, "apiFlavor": "AwsBedrockInvoke"} + + +class TestBedrockFactoryBaseModel: + @pytest.fixture() + def client_settings(self): + import os + from unittest.mock import patch + + from uipath.llm_client.settings.llmgateway import LLMGatewaySettings + + env = { + "LLMGW_URL": "http://test-bedrock", + "LLMGW_SEMANTIC_ORG_ID": "org", + "LLMGW_SEMANTIC_TENANT_ID": "tenant", + "LLMGW_REQUESTING_PRODUCT": "test", + "LLMGW_REQUESTING_FEATURE": "test", + "LLMGW_ACCESS_TOKEN": "dummy-token", + } + with patch.dict(os.environ, env, clear=True): + return LLMGatewaySettings() + + @pytest.fixture(autouse=True) + def _clear_discovery_cache(self): + UiPathBaseSettings._discovery_cache.clear() + yield + UiPathBaseSettings._discovery_cache.clear() + + def _seed(self, client_settings, model_info): + key = client_settings._discovery_cache_key() + client_settings._discovery_cache[key] = [model_info] + + def test_converse_byo_alias_gets_backing_base_model(self, client_settings): + self._seed(client_settings, _BYO_BEDROCK_CONVERSE) + model = get_chat_model( + "AWS - Bedrock", + byo_connection_id="conn-x", + client_settings=client_settings, + ) + assert isinstance(model, UiPathChatBedrockConverse) + assert model.model_id == "AWS - Bedrock" + assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0" + assert model.supports_tool_choice_values == ("auto", "any", "tool") + + from langchain_core.tools import tool + + @tool + def ping() -> str: + """ping.""" + return "pong" + + model.bind_tools([ping], tool_choice="any") + + def test_converse_direct_construction_takes_explicit_backing_model(self, client_settings): + self._seed(client_settings, _BYO_BEDROCK_CONVERSE) + model = UiPathChatBedrockConverse( + model="AWS - Bedrock", + settings=client_settings, + byo_connection_id="conn-x", + base_model="anthropic.claude-sonnet-4-5-20250929-v1:0", + provider="anthropic", + ) + assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0" + assert model.provider == "anthropic" + assert model.supports_tool_choice_values == ("auto", "any", "tool") + + def test_invoke_byo_alias_gets_provider(self, client_settings): + self._seed(client_settings, _BYO_BEDROCK_INVOKE) + model = get_chat_model( + "AWS - Bedrock", + byo_connection_id="conn-x", + client_settings=client_settings, + ) + assert isinstance(model, UiPathChatBedrock) + assert model.model_id == "AWS - Bedrock" + assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0" + assert model.provider == "anthropic" + assert model._get_provider() == "anthropic"