Skip to content

Commit 81d5dfc

Browse files
fix: resolve real customer model for BYO aliases in bedrock factory (#103)
1 parent 2c066c0 commit 81d5dfc

7 files changed

Lines changed: 271 additions & 25 deletions

File tree

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to `uipath_langchain_client` will be documented in this file.
44

5+
## [1.15.1] - 2026-06-30
6+
7+
### Fixed
8+
- 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.
9+
510
## [1.15.0] - 2026-06-25
611

712
### Added
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LangChain Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
3-
__version__ = "1.15.0"
3+
__version__ = "1.15.1"

packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/chat_models.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def _patched_format_data_content_block(block: dict) -> dict:
4848
) from e
4949

5050

51+
def _setup_model_id(values: Any) -> Any:
52+
if isinstance(values, dict) and "model_id" not in values:
53+
model = values.get("model") or values.get("model_name")
54+
if model:
55+
values = {**values, "model_id": model}
56+
return values
57+
58+
5159
class UiPathChatBedrockConverse(UiPathBaseChatModel, ChatBedrockConverse): # type: ignore[override]
5260
api_config: UiPathAPIConfig = UiPathAPIConfig(
5361
api_type=ApiType.COMPLETIONS,
@@ -65,11 +73,7 @@ class UiPathChatBedrockConverse(UiPathBaseChatModel, ChatBedrockConverse): # ty
6573
@model_validator(mode="before")
6674
@classmethod
6775
def setup_model_id(cls, values: Any) -> Any:
68-
if isinstance(values, dict) and "model_id" not in values:
69-
model = values.get("model") or values.get("model_name")
70-
if model:
71-
values = {**values, "model_id": model}
72-
return values
76+
return _setup_model_id(values)
7377

7478
@model_validator(mode="after")
7579
def setup_uipath_client(self) -> Self:
@@ -94,11 +98,7 @@ class UiPathChatBedrock(UiPathBaseChatModel, ChatBedrock): # type: ignore[overr
9498
@model_validator(mode="before")
9599
@classmethod
96100
def setup_model_id(cls, values: Any) -> Any:
97-
if isinstance(values, dict) and "model_id" not in values:
98-
model = values.get("model") or values.get("model_name")
99-
if model:
100-
values = {**values, "model_id": model}
101-
return values
101+
return _setup_model_id(values)
102102

103103
@model_validator(mode="after")
104104
def setup_uipath_client(self) -> Self:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Bedrock backing-model and provider resolution."""
2+
3+
from typing import Any
4+
5+
_BEDROCK_REGIONS = (
6+
"eu",
7+
"us",
8+
"us-gov",
9+
"apac",
10+
"sa",
11+
"amer",
12+
"global",
13+
"jp",
14+
"au",
15+
)
16+
17+
18+
def _split_model_id(model_id: str) -> tuple[str, str] | None:
19+
model_id = model_id.strip()
20+
parts = model_id.split(".")
21+
22+
if parts[0] in _BEDROCK_REGIONS:
23+
parts = parts[1:]
24+
25+
if len(parts) < 2:
26+
return None
27+
28+
provider = parts[0]
29+
if not provider or " " in provider or "/" in provider:
30+
return None
31+
32+
return model_id, provider
33+
34+
35+
def _resolve_backing_model(model_info: dict[str, Any]) -> tuple[str, str] | None:
36+
byo_details = model_info.get("byomDetails") or {}
37+
customer_model = byo_details.get("customerModel")
38+
if isinstance(customer_model, str):
39+
return _split_model_id(customer_model)
40+
return None
41+
42+
43+
def apply_backing_model_detection_hints(
44+
model_kwargs: dict[str, Any], model_info: dict[str, Any]
45+
) -> None:
46+
if "base_model_id" in model_kwargs or "base_model" in model_kwargs:
47+
return
48+
backing = _resolve_backing_model(model_info)
49+
if not backing:
50+
return
51+
base_model_id, provider = backing
52+
model_kwargs["base_model_id"] = base_model_id
53+
model_kwargs.setdefault("provider", provider)

packages/uipath_langchain_client/src/uipath_langchain_client/factory.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
UiPathBaseChatModel,
2727
UiPathBaseEmbeddings,
2828
)
29+
from uipath_langchain_client.clients.bedrock.model_resolution import (
30+
apply_backing_model_detection_hints,
31+
)
2932
from uipath_langchain_client.settings import (
3033
API_FLAVOR_TO_VENDOR_TYPE,
3134
BYOM_TO_ROUTING_FLAVOR,
@@ -205,20 +208,22 @@ def get_chat_model(
205208
**model_kwargs,
206209
)
207210

208-
if api_flavor == ApiFlavor.INVOKE:
209-
if model_family == ModelFamily.ANTHROPIC_CLAUDE:
210-
from uipath_langchain_client.clients.bedrock.chat_models import (
211-
UiPathChatAnthropicBedrock,
212-
)
213-
214-
return UiPathChatAnthropicBedrock(
215-
model=model_name,
216-
settings=client_settings,
217-
byo_connection_id=byo_connection_id,
218-
model_details=model_details,
219-
**model_kwargs,
220-
)
211+
if api_flavor == ApiFlavor.INVOKE and model_family == ModelFamily.ANTHROPIC_CLAUDE:
212+
from uipath_langchain_client.clients.bedrock.chat_models import (
213+
UiPathChatAnthropicBedrock,
214+
)
221215

216+
return UiPathChatAnthropicBedrock(
217+
model=model_name,
218+
settings=client_settings,
219+
byo_connection_id=byo_connection_id,
220+
model_details=model_details,
221+
**model_kwargs,
222+
)
223+
224+
apply_backing_model_detection_hints(model_kwargs, model_info)
225+
226+
if api_flavor == ApiFlavor.INVOKE:
222227
from uipath_langchain_client.clients.bedrock.chat_models import (
223228
UiPathChatBedrock,
224229
)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Unit tests for Bedrock backing-model resolution."""
2+
3+
import pytest
4+
from uipath_langchain_client.clients.bedrock.model_resolution import (
5+
apply_backing_model_detection_hints,
6+
)
7+
8+
9+
class TestApplyBackingModelDetectionHints:
10+
@pytest.mark.parametrize(
11+
"customer_model,expected_provider",
12+
[
13+
("anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic"),
14+
("global.anthropic.claude-sonnet-4-6", "anthropic"),
15+
("amazon.nova-pro-v1:0", "amazon"),
16+
],
17+
)
18+
def test_byo_uses_customer_model(self, customer_model, expected_provider):
19+
kwargs: dict = {}
20+
apply_backing_model_detection_hints(
21+
kwargs,
22+
{
23+
"modelName": "AWS - Bedrock",
24+
"byomDetails": {
25+
"customerModel": customer_model,
26+
"integrationServiceConnectionId": "conn-x",
27+
},
28+
},
29+
)
30+
assert kwargs["base_model_id"] == customer_model
31+
assert kwargs["provider"] == expected_provider
32+
33+
def test_unparseable_customer_model_sets_no_hints(self):
34+
kwargs: dict = {}
35+
apply_backing_model_detection_hints(
36+
kwargs,
37+
{
38+
"modelName": "AWS - Bedrock",
39+
"byomDetails": {"customerModel": "my-claude-sonnet-4-5"},
40+
},
41+
)
42+
assert "base_model_id" not in kwargs
43+
assert "provider" not in kwargs
44+
45+
def test_non_byo_model_sets_no_hints(self):
46+
kwargs: dict = {}
47+
apply_backing_model_detection_hints(
48+
kwargs,
49+
{
50+
"modelName": "anthropic.claude-3-5-sonnet-20240620-v1:0",
51+
"byomDetails": None,
52+
},
53+
)
54+
assert "base_model_id" not in kwargs
55+
assert "provider" not in kwargs
56+
57+
def test_byo_alias_without_customer_model_sets_no_hints(self):
58+
kwargs: dict = {}
59+
apply_backing_model_detection_hints(
60+
kwargs,
61+
{
62+
"modelName": "VeryCustomBedddrockAlias",
63+
"byomDetails": {"integrationServiceConnectionId": "conn-x"},
64+
},
65+
)
66+
assert "base_model_id" not in kwargs
67+
assert "provider" not in kwargs
68+
69+
def test_does_not_override_caller_supplied_hints(self):
70+
kwargs = {"base_model_id": "amazon.nova-pro-v1:0", "provider": "amazon"}
71+
apply_backing_model_detection_hints(
72+
kwargs,
73+
{"byomDetails": {"customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0"}},
74+
)
75+
assert kwargs["base_model_id"] == "amazon.nova-pro-v1:0"
76+
assert kwargs["provider"] == "amazon"
77+
78+
def test_caller_base_model_alias_is_not_shadowed_by_base_model_id(self):
79+
kwargs = {"base_model": "amazon.nova-pro-v1:0"}
80+
apply_backing_model_detection_hints(
81+
kwargs,
82+
{"byomDetails": {"customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0"}},
83+
)
84+
assert "base_model_id" not in kwargs
85+
assert kwargs["base_model"] == "amazon.nova-pro-v1:0"

tests/langchain/features/test_factory_function.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
from unittest.mock import MagicMock
22

33
import pytest
4+
from uipath_langchain_client.clients.bedrock.chat_models import (
5+
UiPathChatBedrock,
6+
UiPathChatBedrockConverse,
7+
)
48
from uipath_langchain_client.clients.normalized.chat_models import UiPathChat
59
from uipath_langchain_client.clients.normalized.embeddings import UiPathEmbeddings
6-
from uipath_langchain_client.factory import get_chat_model, get_embedding_model
10+
from uipath_langchain_client.factory import (
11+
get_chat_model,
12+
get_embedding_model,
13+
)
714

815
from tests.langchain.conftest import COMPLETION_MODEL_NAMES, EMBEDDING_MODEL_NAMES
916
from uipath.llm_client.settings import ApiFlavor, UiPathBaseSettings, VendorType
@@ -379,3 +386,94 @@ def test_anthropic_messages_routes_to_uipath_chat_anthropic(
379386
)
380387
assert captured["vendor_type"] == VendorType.AWSBEDROCK
381388
assert captured["api_flavor"] == ApiFlavor.ANTHROPIC_MESSAGES
389+
390+
391+
_BYO_BEDROCK_CONVERSE = {
392+
"modelName": "AWS - Bedrock",
393+
"vendor": "AwsBedrock",
394+
"modelFamily": None,
395+
"apiFlavor": None,
396+
"modelSubscriptionType": "BYOMAdded",
397+
"byomDetails": {
398+
"customerModel": "anthropic.claude-sonnet-4-5-20250929-v1:0",
399+
"integrationServiceConnectionId": "conn-x",
400+
},
401+
}
402+
_BYO_BEDROCK_INVOKE = {**_BYO_BEDROCK_CONVERSE, "apiFlavor": "AwsBedrockInvoke"}
403+
404+
405+
class TestBedrockFactoryBaseModel:
406+
@pytest.fixture()
407+
def client_settings(self):
408+
import os
409+
from unittest.mock import patch
410+
411+
from uipath.llm_client.settings.llmgateway import LLMGatewaySettings
412+
413+
env = {
414+
"LLMGW_URL": "http://test-bedrock",
415+
"LLMGW_SEMANTIC_ORG_ID": "org",
416+
"LLMGW_SEMANTIC_TENANT_ID": "tenant",
417+
"LLMGW_REQUESTING_PRODUCT": "test",
418+
"LLMGW_REQUESTING_FEATURE": "test",
419+
"LLMGW_ACCESS_TOKEN": "dummy-token",
420+
}
421+
with patch.dict(os.environ, env, clear=True):
422+
return LLMGatewaySettings()
423+
424+
@pytest.fixture(autouse=True)
425+
def _clear_discovery_cache(self):
426+
UiPathBaseSettings._discovery_cache.clear()
427+
yield
428+
UiPathBaseSettings._discovery_cache.clear()
429+
430+
def _seed(self, client_settings, model_info):
431+
key = client_settings._discovery_cache_key()
432+
client_settings._discovery_cache[key] = [model_info]
433+
434+
def test_converse_byo_alias_gets_backing_base_model(self, client_settings):
435+
self._seed(client_settings, _BYO_BEDROCK_CONVERSE)
436+
model = get_chat_model(
437+
"AWS - Bedrock",
438+
byo_connection_id="conn-x",
439+
client_settings=client_settings,
440+
)
441+
assert isinstance(model, UiPathChatBedrockConverse)
442+
assert model.model_id == "AWS - Bedrock"
443+
assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0"
444+
assert model.supports_tool_choice_values == ("auto", "any", "tool")
445+
446+
from langchain_core.tools import tool
447+
448+
@tool
449+
def ping() -> str:
450+
"""ping."""
451+
return "pong"
452+
453+
model.bind_tools([ping], tool_choice="any")
454+
455+
def test_converse_direct_construction_takes_explicit_backing_model(self, client_settings):
456+
self._seed(client_settings, _BYO_BEDROCK_CONVERSE)
457+
model = UiPathChatBedrockConverse(
458+
model="AWS - Bedrock",
459+
settings=client_settings,
460+
byo_connection_id="conn-x",
461+
base_model="anthropic.claude-sonnet-4-5-20250929-v1:0",
462+
provider="anthropic",
463+
)
464+
assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0"
465+
assert model.provider == "anthropic"
466+
assert model.supports_tool_choice_values == ("auto", "any", "tool")
467+
468+
def test_invoke_byo_alias_gets_provider(self, client_settings):
469+
self._seed(client_settings, _BYO_BEDROCK_INVOKE)
470+
model = get_chat_model(
471+
"AWS - Bedrock",
472+
byo_connection_id="conn-x",
473+
client_settings=client_settings,
474+
)
475+
assert isinstance(model, UiPathChatBedrock)
476+
assert model.model_id == "AWS - Bedrock"
477+
assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0"
478+
assert model.provider == "anthropic"
479+
assert model._get_provider() == "anthropic"

0 commit comments

Comments
 (0)