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
5 changes: 5 additions & 0 deletions packages/uipath_langchain_client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down
85 changes: 85 additions & 0 deletions tests/langchain/clients/bedrock/test_model_resolution.py
Original file line number Diff line number Diff line change
@@ -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"
100 changes: 99 additions & 1 deletion tests/langchain/features/test_factory_function.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Loading