Skip to content

Commit 6dbaeef

Browse files
authored
Fixes on platform settings (#42)
1 parent 8932042 commit 6dbaeef

10 files changed

Lines changed: 233 additions & 9 deletions

File tree

CHANGELOG.md

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

33
All notable changes to `uipath_llm_client` (core package) will be documented in this file.
44

5+
## [1.5.1] - 2026-03-17
6+
7+
### Fix
8+
- Added error message for normalized embeddings on UiPath Platform (AgentHub/Orchestrator) as there is no supported endpoint
9+
- Fix endpoints for platform to remove api version
10+
511
## [1.5.0] - 2026-03-16
612

713
### Stable Version 1.5.0

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.5.1] - 2026-03-17
6+
7+
### Fixes
8+
- Fixes to core package, version bump
9+
510
## [1.5.0] - 2026-03-16
611

712
### Stable Version 1.5.0

packages/uipath_langchain_client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"langchain>=1.2.12",
9-
"uipath-llm-client>=1.5.0",
9+
"uipath-llm-client>=1.5.1",
1010
]
1111

1212
[project.optional-dependencies]
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.5.0"
3+
__version__ = "1.5.1"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LLM Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services."
3-
__version__ = "1.5.0"
3+
__version__ = "1.5.1"

src/uipath/llm_client/settings/platform/settings.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ def validate_environment(self) -> Self:
7373
self.client_id = parsed_token_data.get("client_id", None)
7474
return self
7575

76+
@staticmethod
77+
def _format_endpoint(endpoint: str, **kwargs: str | None) -> str:
78+
"""Format an endpoint template, stripping query params with None values."""
79+
# Remove query parameters whose values are None
80+
if "?" in endpoint:
81+
base, query = endpoint.split("?", 1)
82+
params = [
83+
p
84+
for p in query.split("&")
85+
if not any(f"{{{k}}}" in p for k, v in kwargs.items() if v is None)
86+
]
87+
endpoint = f"{base}?{'&'.join(params)}" if params else base
88+
return endpoint.format(**{k: v for k, v in kwargs.items() if v is not None})
89+
7690
@override
7791
def build_base_url(
7892
self,
@@ -85,12 +99,24 @@ def build_base_url(
8599
assert api_config is not None
86100
if api_config.routing_mode == "normalized" and api_config.api_type == "completions":
87101
url = f"{self.base_url}/{EndpointManager.get_normalized_endpoint()}"
102+
elif api_config.routing_mode == "normalized" and api_config.api_type == "embeddings":
103+
raise ValueError(
104+
"Normalized embeddings are not supported on UiPath Platform (AgentHub/Orchestrator). "
105+
"Use passthrough routing mode for embeddings instead."
106+
)
107+
elif api_config.routing_mode == "passthrough" and api_config.api_type == "completions":
108+
endpoint = EndpointManager.get_vendor_endpoint()
109+
url = f"{self.base_url}/{self._format_endpoint(endpoint, model=model_name, vendor=api_config.vendor_type, api_version=api_config.api_version)}"
88110
elif api_config.routing_mode == "passthrough" and api_config.api_type == "embeddings":
89-
assert api_config.api_version is not None
90-
url = f"{self.base_url}/{EndpointManager.get_embeddings_endpoint().format(model=model_name, api_version=api_config.api_version)}"
111+
if api_config.vendor_type is not None and api_config.vendor_type != "openai":
112+
raise ValueError(
113+
f"Platform embeddings endpoint only supports OpenAI-compatible models, "
114+
f"got vendor_type='{api_config.vendor_type}'."
115+
)
116+
endpoint = EndpointManager.get_embeddings_endpoint()
117+
url = f"{self.base_url}/{self._format_endpoint(endpoint, model=model_name, api_version=api_config.api_version)}"
91118
else:
92-
assert api_config.vendor_type is not None
93-
url = f"{self.base_url}/{EndpointManager.get_vendor_endpoint().format(model=model_name, vendor=api_config.vendor_type)}"
119+
raise ValueError(f"Invalid API configuration: {api_config}")
94120
return url
95121

96122
@override

tests/cassettes.db

14.1 MB
Binary file not shown.

tests/conftest.py

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

33
from uipath.llm_client.settings import UiPathBaseSettings
44
from uipath.llm_client.settings.llmgateway import LLMGatewaySettings
5+
from uipath.llm_client.settings.platform import PlatformSettings
56

67

78
@pytest.fixture(autouse=True, scope="session")
@@ -60,5 +61,7 @@ def client_settings(request: pytest.FixtureRequest) -> UiPathBaseSettings:
6061
match request.param:
6162
case "llmgw":
6263
return LLMGatewaySettings()
64+
case "agenthub":
65+
return PlatformSettings()
6366
case _:
6467
raise ValueError(f"Invalid client type: {request.param}")

tests/core/test_base_client.py

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,33 @@ class TestPlatformSettings:
480480
def test_build_base_url_passthrough(
481481
self, platform_env_vars, mock_platform_auth, passthrough_api_config
482482
):
483-
"""Test build_base_url for passthrough mode."""
483+
"""Test build_base_url for passthrough completions mode."""
484484
with patch.dict(os.environ, platform_env_vars, clear=True):
485485
settings = PlatformSettings()
486486
url = settings.build_base_url(
487487
model_name="gpt-4o",
488488
api_config=passthrough_api_config,
489489
)
490-
assert "agenthub_/llm/raw/vendor/openai/model/gpt-4o/completions" in url
490+
assert "llm/raw/vendor/openai/model/gpt-4o/completions" in url
491+
492+
def test_build_base_url_passthrough_with_api_version(
493+
self, platform_env_vars, mock_platform_auth
494+
):
495+
"""Test build_base_url for passthrough completions with api_version (vendor endpoint ignores it)."""
496+
api_config = UiPathAPIConfig(
497+
api_type=ApiType.COMPLETIONS,
498+
routing_mode=RoutingMode.PASSTHROUGH,
499+
vendor_type="openai",
500+
api_version="2025-03-01",
501+
)
502+
with patch.dict(os.environ, platform_env_vars, clear=True):
503+
settings = PlatformSettings()
504+
url = settings.build_base_url(
505+
model_name="gpt-4o",
506+
api_config=api_config,
507+
)
508+
assert "llm/raw/vendor/openai/model/gpt-4o/completions" in url
509+
assert "api-version" not in url
491510

492511
def test_build_base_url_normalized(
493512
self, platform_env_vars, mock_platform_auth, normalized_api_config
@@ -532,6 +551,152 @@ def test_build_auth_pipeline_returns_auth(self, platform_env_vars, mock_platform
532551
auth = settings.build_auth_pipeline()
533552
assert isinstance(auth, Auth)
534553

554+
def test_build_auth_pipeline_with_access_token(self, platform_env_vars, mock_platform_auth):
555+
"""Test auth pipeline uses access_token when provided."""
556+
from uipath.llm_client.settings.platform.auth import PlatformAuth
557+
558+
with patch.dict(os.environ, platform_env_vars, clear=True):
559+
settings = PlatformSettings()
560+
auth = settings.build_auth_pipeline()
561+
assert isinstance(auth, PlatformAuth)
562+
563+
def test_build_base_url_passthrough_embeddings(self, platform_env_vars, mock_platform_auth):
564+
"""Test build_base_url for passthrough embeddings with api_version."""
565+
api_config = UiPathAPIConfig(
566+
api_type=ApiType.EMBEDDINGS,
567+
routing_mode=RoutingMode.PASSTHROUGH,
568+
vendor_type="openai",
569+
api_version="2024-02-01",
570+
)
571+
with patch.dict(os.environ, platform_env_vars, clear=True):
572+
settings = PlatformSettings()
573+
url = settings.build_base_url(
574+
model_name="text-embedding-3-large",
575+
api_config=api_config,
576+
)
577+
assert "embeddings" in url
578+
assert "text-embedding-3-large" in url
579+
assert "api-version=2024-02-01" in url
580+
581+
def test_build_base_url_passthrough_embeddings_no_api_version(
582+
self, platform_env_vars, mock_platform_auth
583+
):
584+
"""Test build_base_url for passthrough embeddings without api_version."""
585+
api_config = UiPathAPIConfig(
586+
api_type=ApiType.EMBEDDINGS,
587+
routing_mode=RoutingMode.PASSTHROUGH,
588+
vendor_type="openai",
589+
)
590+
with patch.dict(os.environ, platform_env_vars, clear=True):
591+
settings = PlatformSettings()
592+
url = settings.build_base_url(
593+
model_name="text-embedding-3-large",
594+
api_config=api_config,
595+
)
596+
assert "embeddings" in url
597+
assert "text-embedding-3-large" in url
598+
assert "api-version" not in url
599+
600+
def test_build_base_url_passthrough_embeddings_non_openai_raises(
601+
self, platform_env_vars, mock_platform_auth
602+
):
603+
"""Test build_base_url raises for non-OpenAI passthrough embeddings."""
604+
api_config = UiPathAPIConfig(
605+
api_type=ApiType.EMBEDDINGS,
606+
routing_mode=RoutingMode.PASSTHROUGH,
607+
vendor_type="vertexai",
608+
)
609+
with patch.dict(os.environ, platform_env_vars, clear=True):
610+
settings = PlatformSettings()
611+
with pytest.raises(ValueError, match="only supports OpenAI-compatible models"):
612+
settings.build_base_url(
613+
model_name="gemini-embedding-001",
614+
api_config=api_config,
615+
)
616+
617+
def test_build_base_url_normalized_embeddings_raises(
618+
self, platform_env_vars, mock_platform_auth
619+
):
620+
"""Test build_base_url raises ValueError for normalized embeddings."""
621+
normalized_embeddings_config = UiPathAPIConfig(
622+
api_type=ApiType.EMBEDDINGS,
623+
routing_mode=RoutingMode.NORMALIZED,
624+
)
625+
with patch.dict(os.environ, platform_env_vars, clear=True):
626+
settings = PlatformSettings()
627+
with pytest.raises(ValueError, match="Normalized embeddings are not supported"):
628+
settings.build_base_url(
629+
model_name="text-embedding-3-large",
630+
api_config=normalized_embeddings_config,
631+
)
632+
633+
def test_build_base_url_requires_model_name(
634+
self, platform_env_vars, mock_platform_auth, normalized_api_config
635+
):
636+
"""Test build_base_url asserts model_name is not None."""
637+
with patch.dict(os.environ, platform_env_vars, clear=True):
638+
settings = PlatformSettings()
639+
with pytest.raises(AssertionError):
640+
settings.build_base_url(model_name=None, api_config=normalized_api_config)
641+
642+
def test_build_base_url_requires_api_config(self, platform_env_vars, mock_platform_auth):
643+
"""Test build_base_url asserts api_config is not None."""
644+
with patch.dict(os.environ, platform_env_vars, clear=True):
645+
settings = PlatformSettings()
646+
with pytest.raises(AssertionError):
647+
settings.build_base_url(model_name="gpt-4o", api_config=None)
648+
649+
def test_build_auth_headers_empty_when_no_optional(self, platform_env_vars, mock_platform_auth):
650+
"""Test build_auth_headers with no optional tracing fields set."""
651+
env = {**platform_env_vars, "UIPATH_AGENTHUB_CONFIG": ""}
652+
with patch.dict(os.environ, env, clear=True):
653+
settings = PlatformSettings()
654+
# Override to empty to test the falsy path
655+
settings.agenthub_config = ""
656+
settings.process_key = None
657+
settings.job_key = None
658+
headers = settings.build_auth_headers()
659+
assert headers == {}
660+
661+
def test_validation_requires_all_fields(self, mock_platform_auth):
662+
"""Test validation fails without required fields."""
663+
env = {
664+
"UIPATH_ACCESS_TOKEN": "test-access-token",
665+
# Missing base_url, tenant_id, organization_id
666+
}
667+
with patch.dict(os.environ, env, clear=True):
668+
with pytest.raises(ValueError, match="Base URL, access token, tenant ID"):
669+
PlatformSettings()
670+
671+
def test_validation_fails_on_expired_token(self):
672+
"""Test validation fails when access token is expired."""
673+
with (
674+
patch(
675+
"uipath.llm_client.settings.platform.settings.is_token_expired",
676+
return_value=True,
677+
),
678+
patch(
679+
"uipath.llm_client.settings.platform.settings.parse_access_token",
680+
return_value={"client_id": "test-client-id"},
681+
),
682+
):
683+
env = {
684+
"UIPATH_ACCESS_TOKEN": "test-access-token",
685+
"UIPATH_URL": "https://cloud.uipath.com/org/tenant",
686+
"UIPATH_TENANT_ID": "test-tenant-id",
687+
"UIPATH_ORGANIZATION_ID": "test-org-id",
688+
}
689+
with patch.dict(os.environ, env, clear=True):
690+
with pytest.raises(ValueError, match="Access token is expired"):
691+
PlatformSettings()
692+
693+
def test_validate_byo_model_is_noop(self, platform_env_vars, mock_platform_auth):
694+
"""Test validate_byo_model does nothing (no-op)."""
695+
with patch.dict(os.environ, platform_env_vars, clear=True):
696+
settings = PlatformSettings()
697+
result = settings.validate_byo_model({"modelName": "custom-model"})
698+
assert result is None
699+
535700

536701
# ============================================================================
537702
# Test Platform Auth Refresh Logic

tests/langchain/test_provider_integrations.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
UiPathChatBedrockConverse,
1313
)
1414
from uipath_langchain_client.clients.google.chat_models import UiPathChatGoogleGenerativeAI
15+
from uipath_langchain_client.clients.google.embeddings import UiPathGoogleGenerativeAIEmbeddings
1516
from uipath_langchain_client.clients.normalized.chat_models import UiPathChat
17+
from uipath_langchain_client.clients.normalized.embeddings import UiPathEmbeddings
1618
from uipath_langchain_client.clients.vertexai.chat_models import UiPathChatAnthropicVertex
1719

1820
from tests.langchain.utils import search_accommodation, search_attractions, search_flights
21+
from uipath.llm_client.settings import PlatformSettings
1922

2023

2124
@pytest.mark.asyncio
@@ -503,6 +506,22 @@ class TestIntegrationEmbeddings(EmbeddingsIntegrationTests):
503506
def setup_models(self, embeddings_config: tuple[type[Embeddings], dict[str, Any]]):
504507
self._embeddings_class, self._embeddings_kwargs = embeddings_config
505508

509+
@pytest.fixture(autouse=True)
510+
def skip_on_specific_configs(
511+
self,
512+
embeddings_config: tuple[type[Embeddings], dict[str, Any]],
513+
) -> None:
514+
model_class, model_kwargs = embeddings_config
515+
client_settings = model_kwargs.get("client_settings")
516+
if model_class == UiPathEmbeddings and isinstance(client_settings, PlatformSettings):
517+
pytest.skip(
518+
"Normalized embeddings are not supported on UiPath Platform (AgentHub/Orchestrator)"
519+
)
520+
if model_class == UiPathGoogleGenerativeAIEmbeddings and isinstance(
521+
client_settings, PlatformSettings
522+
):
523+
pytest.skip("Platform embeddings endpoint only supports OpenAI-compatible models")
524+
506525
@property
507526
def embeddings_class(self) -> type[Embeddings]:
508527
return self._embeddings_class

0 commit comments

Comments
 (0)